RBAC系统

关于权限

权限(Permission):

用户使用系统的合法性的判断依据

作用:

使用该系统的依据

组成:

认证(鉴权)+授权

认证:

校验合法性

授权:

认证完成后,该用户具备的操作范围

用户: 查询个人信息 / 存取

工作人员: 购买基金

经理: 分配工作人员的权限

 

操作

用户名+密码(加密)

用户名(手机号)+密码 完成认证, 通过不同的数据表(tb_user / tb_manager / tb_admin)判断用户的权限

用户名+密码(加密)+盐

用户名+密码+盐值(salt) 密码: 123456 盐值: 2345 md5(md5(123456)+md5(2345))

通过转发实现控制(jsp)

登录完成后转发管理页面(主要针对jsp)

jsp: 放在/WEB-INF/jsp/index.jsp

通过请求头携带token

前后端分离:

前端控制页面的跳转,通过js把特征标记存储在localStorage/sessionStorage,在每一次打开页面/请求接口的时候,通过请求头携带该标记(token)

if(!window.localStorage.getItem('auth')){
     window.location.href='/';
     return false;
}

当前端存储名为token, 值在java中产生(UUID)

  String phone = req.getParameter("phone");

       //登录成功
       // 生成token(唯一性/ 复杂度)
       String token = UUID.randomUUID().toString().replace("-", "");
       // 服务器把token值存储在session中
       HttpSession session = req.getSession();
       session.setAttribute("token",token);

 

当访问业务操作中

 // 查询业务要求前端携带token的请求头
   String token = req.getHeader("token");
   if(StringUtil.isEmpty(token)){
       throw new BusinessException(ExceptionCode.WRONG_TOKEN);
  }

   // 校验token的合法性
   HttpSession session = req.getSession();
   Object sysToken = session.getAttribute("token");
   // 用户携带的token请求头和session中存的token不一致
   if(!token.equals(sysToken)){
       throw new BusinessException(ExceptionCode.WRONG_TOKEN);
  }

 

过滤器进行判断

利用过滤器设置权限的统一判断

@Override
   public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    // 这里省略了放行静态资源的部分
     // 通过 request.getRequestURI() 判断请求的资源类型
     
       HttpServletRequest request = (HttpServletRequest) req;
       String token = request.getHeader("token");

       HttpSession session = request.getSession();
       Object sysToken = session.getAttribute("token");
       
       // 满足token条件才放行
       if(token!=null && sysToken!=null && token.equals(sysToken)){
           chain.doFilter(req, resp);
      }
     
  }

 

利用拦截器判断token(SpringMVC)

@Component
public class TokenInterceptor implements HandlerInterceptor {

   /**
    * 拦截前的处理操作
    * @param httpServletRequest request对象(header/session)
    * @param httpServletResponse response对象(转发, 通过全局异常处理已经可以实现给前端的异常响应)
    * @param o 封装的一个方法的HandlerMethod对象:
    *         HandlerMethod对象通过getMethod方法获得Method
    *         /user/login->login()-> 反射得到方法信息->注解
    *           例如: 设计一个自定义注解: @NeedToken
    * @return true表示放行,反之表示拦死
    * @throws Exception 把所有的异常都整合为一个,因为有ExceptionController的存在,会被集中处理
    */
   @Override
   public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
       // 拦截器误拦了资源
       System.out.println("o:"+o.getClass());

       // 如果o注入的目标不是方法, 直接选择放行
       if(!(o instanceof HandlerMethod)){
           return true;
      }



      // 带有NeedToken注解时可以这样操作:
       // selectById / update / delete
     /* HandlerMethod handlerMethod = (HandlerMethod) o;
       Method method = handlerMethod.getMethod();
       NeedToken declaredAnnotation = method.getDeclaredAnnotation(NeedToken.class);

       // 判断该方法上有没有需要的注解
       // 如果为true,就表示必须校验token
       // 否则: 虽然不是注册/登录,但也不需要校验:
       boolean annotationPresent = method.isAnnotationPresent(NeedToken.class);
       // 如果该方法中存在指定的注解,则判断token
       if(annotationPresent && declaredAnnotation.required()){
           // 方法存在@NeedToken, 就需要再校验token

           // 前端通过header
           String token = httpServletRequest.getHeader("token");

           // 服务器中在session存的token
           HttpSession session = httpServletRequest.getSession();
           Object sysToken = session.getAttribute("token");

           // 判断token的异常情况,如果校验不通过,则抛出异常(拦死)
           if(StringUtils.isEmpty(token)){
               throw new BusinessException(ExceptionCode.EMPTY_TOKEN);
           }else if(!token.equals(sysToken)){
               throw new BusinessException(ExceptionCode.WRONG_TOKEN);
           }

           // 如果校验通过,则放行
           return true;

       }else{
           // 选择直接放行
           return true;
       }*/


       // 前端通过header
       String token = httpServletRequest.getHeader("token");

       // 服务器中在session存的token
       HttpSession session = httpServletRequest.getSession();
       Object sysToken = session.getAttribute("token");

       // 判断token的异常情况,如果校验不通过,则抛出异常(拦死)
       if(StringUtils.isEmpty(token)){
           throw new BusinessException(ExceptionCode.EMPTY_TOKEN);
      }else if(!token.equals(sysToken)){
           throw new BusinessException(ExceptionCode.WRONG_TOKEN);
      }

       // 如果校验通过,则放行
       return true;
  }

   /**
    * 拦截后放行的处理
    * @param httpServletRequest
    * @param httpServletResponse
    * @param o
    * @param modelAndView
    * @throws Exception
    */
   @Override
   public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

  }

   /**
    * 拦截后的处理
    * @param httpServletRequest
    * @param httpServletResponse
    * @param o
    * @throws Exception
    */
   @Override
   public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

  }
}

 

 

RBAC系统

需求:

前端需要权限树(tree), 不同的账户登录后看到的权限结构不同

管理员看到的界面(所有权限)

 

 

只被授予了Banner管理权限的工作人员看到的界面:

 

 

而权限的分配方案是由后端生成返回给前端去渲染的

问题:

如果我们按照一般思路,将用户和权限之间以多对多的方式来查询,会发现一个问题:

如果两个(多个)用户的权限一致,这种情况在企业中是很常见的,那么我们用多对多的方式来实现:

 

解决方案:

那么如果多个用户权限相同, 可否把用户和权限的绑定关系分开: 先给用户分配角色, 再给角色分配权限 ,这样就可以在用户分配相同权限的时候,通过指定角色即可实现

 

概念:

角色(Role): 在分配权限的时候,先给角色分配权限(menu) , 然后给用户指定角色

RBAC: Role-based Access Control 基于角色的权限控制

实现:

数据库脚本:

/*
Navicat MySQL Data Transfer

Source Server         : bj291
Source Server Version : 50717
Source Host           : localhost:3306
Source Database       : ehome

Target Server Type   : MYSQL
Target Server Version : 50717
File Encoding         : 65001

Date: 2021-10-22 15:16:07
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tb_menu
-- ----------------------------
DROP TABLE IF EXISTS `tb_menu`;
CREATE TABLE `tb_menu` (
`menu_id` int(11) NOT NULL AUTO_INCREMENT,
`menu_pid` int(11) NOT NULL DEFAULT '0',
`menu_name` varchar(30) NOT NULL,
`menu_url` varchar(50) DEFAULT NULL,
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_menu
-- ----------------------------
INSERT INTO `tb_menu` VALUES ('1', '0', '用户管理', '/user');
INSERT INTO `tb_menu` VALUES ('2', '1', '添加用户', '/user/add');
INSERT INTO `tb_menu` VALUES ('3', '1', '删除用户', '/user/del');
INSERT INTO `tb_menu` VALUES ('4', '0', '权限管理', '/permission');
INSERT INTO `tb_menu` VALUES ('5', '4', '添加权限', '/permission/add');
INSERT INTO `tb_menu` VALUES ('6', '4', '删除权限', '/permission/del');
INSERT INTO `tb_menu` VALUES ('7', '0', '优惠券管理', '/cpn');
INSERT INTO `tb_menu` VALUES ('8', '7', '添加优惠券', '/cpn/add');
INSERT INTO `tb_menu` VALUES ('9', '7', '查询优惠券', '/cpn/selectAll');

-- ----------------------------
-- Table structure for tb_role
-- ----------------------------
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色表主键自增',
`role_name` varchar(30) DEFAULT NULL,
`role_remark` varchar(20) DEFAULT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_role
-- ----------------------------
INSERT INTO `tb_role` VALUES ('1', 'ROLE_ADMIN', '超级管理员');
INSERT INTO `tb_role` VALUES ('2', 'ROLE_MGR', '普通管理员');

-- ----------------------------
-- Table structure for tb_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `tb_role_menu`;
CREATE TABLE `tb_role_menu` (
`role_menu_id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL,
`menu_id` int(11) NOT NULL,
PRIMARY KEY (`role_menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_role_menu
-- ----------------------------
INSERT INTO `tb_role_menu` VALUES ('1', '1', '1');
INSERT INTO `tb_role_menu` VALUES ('2', '1', '2');
INSERT INTO `tb_role_menu` VALUES ('3', '1', '3');
INSERT INTO `tb_role_menu` VALUES ('4', '1', '4');
INSERT INTO `tb_role_menu` VALUES ('5', '1', '5');
INSERT INTO `tb_role_menu` VALUES ('6', '1', '6');
INSERT INTO `tb_role_menu` VALUES ('7', '2', '7');
INSERT INTO `tb_role_menu` VALUES ('8', '2', '8');
INSERT INTO `tb_role_menu` VALUES ('9', '2', '9');

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(20) DEFAULT NULL,
`user_phone` varchar(15) DEFAULT NULL,
`user_regTime` date DEFAULT NULL,
`user_birth` datetime DEFAULT NULL,
`user_sex` varchar(3) DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `unique_user_phone` (`user_phone`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES ('1', '张三', '13344445555', '2021-05-21', '2021-05-03 00:00:00', '女');
INSERT INTO `tb_user` VALUES ('2', '李四喜', '13344446666', null, null, '男');
INSERT INTO `tb_user` VALUES ('3', '王老五', '13444446668', '2021-05-20', null, '女');

-- ----------------------------
-- Table structure for tb_user_role
-- ----------------------------
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role` (
`user_role_id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_user_role
-- ----------------------------
INSERT INTO `tb_user_role` VALUES ('1', '1', '1');
INSERT INTO `tb_user_role` VALUES ('2', '1', '2');

-- ----------------------------

 

查询:

-- 用户输入手机号(账号) , 查询所有的权限
select p.*
from tb_admin a
LEFT JOIN admin_role ar
ON a.id = ar.aid
LEFT JOIN role r
ON ar.rid = r.id
LEFT JOIN role_permission rp
ON r.id = rp.rid
LEFT JOIN permission p
ON rp.pid = p.id
WHERE a.phone = '134'

 

将查询放在mapper中实现,通过service调用并给controller提供数据,最终效果:

// 给前端返回的数据
{
code:1,
msg:"",
//菜单列表(扁平化处理)
data:[
{menuId:1,pid:0,menuName:"用户管理",menuUrl:"/user"},
{menuId:2,pid:1,menuName:"添加用户",menuUrl:"/user/add"},
{menuId:3,pid:1,menuName:"删除用户",menuUrl:"/user/del"},
{menuId:4,pid:0,menuName:"权限管理",menuUrl:"/permission"},
{menuId:5,pid:4,menuName:"添加权限",menuUrl:"/permission/add"}
]
}

 

实体类

/**
	权限类型的实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission implements Serializable {
    private Integer id;
    private Integer pid;
    private String desc;
    private String name;
    private String url;
}

 

PermissionMapper接口

public interface PermissionMapper  {

    public List<Permission> selectAllByPhone(String phone);
}

 

PermissionMapper.xml

<mapper namespace="com.gxa.hualian.mapper.PermissionMapper">

    <select id="selectAllByPhone" parameterType="string" resultType="Permission">
        select p.*
        from tb_admin a
                 LEFT JOIN admin_role ar
                     ON a.id = ar.aid
                 LEFT JOIN role r
                     ON ar.rid = r.id
                 LEFT JOIN role_permission rp
                     ON r.id = rp.rid
                 LEFT JOIN permission p
                     ON rp.pid = p.id
        WHERE a.phone = #{arg0}
    </select>

</mapper>

 

测试用例:

@WebAppConfiguration
@ContextConfiguration("classpath:spring.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class PermissionMapperTest {

    @Autowired
    PermissionMapper permissionMapper;

    @Test
    public void testSelectById(){
        List<Permission> permissions = permissionMapper.selectAllByPhone("134");
        System.out.println(permissions);
    }
}

 

结果:

JDBC Connection [com.mysql.jdbc.JDBC4Connection@4eb1c69] will not be managed by Spring
==>  Preparing: select p.* from tb_admin a LEFT JOIN admin_role ar ON a.id = ar.aid LEFT JOIN role r ON ar.rid = r.id LEFT JOIN role_permission rp ON r.id = rp.rid LEFT JOIN permission p ON rp.pid = p.id WHERE a.phone = ? 
==> Parameters: 134(String)

六月 07, 2022 12:11:17 下午 org.springframework.web.context.support.GenericWebApplicationContext doClose
信息: Closing org.springframework.web.context.support.GenericWebApplicationContext@5622fdf: startup date [Tue Jun 07 12:11:13 CST 2022]; root of context hierarchy
<==    Columns: id, desc, pid, url, name
<==        Row: 1, 用户管理, 0, /user, 
<==        Row: 2, 添加用户, 1, /user/add, user:add
<==        Row: 3, 删除用户, 1, /user/delete, user:delete
<==      Total: 3
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3ae9d1e2]
[Permission(id=1, pid=0, desc=用户管理, name=, url=/user), Permission(id=2, pid=1, desc=添加用户, name=user:add, url=/user/add), Permission(id=3, pid=1, desc=删除用户, name=user:delete, url=/user/delete)]
 

 

posted @ 2022-08-11 16:13  码上冲  阅读(38)  评论(0)    收藏  举报