文章

RBAC权限管理

RBAC权限管理

Role-Based Access Control:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。在后端开发场景下,权限即表示对某path的可访问性or能访问到什么地步。具体到实现上,一是验证这个请求能否访问这个path,二是验证这个请求的参数是否符合他的权限。

在招新平台中,具体的实现思路是:

包含三个基本单位:路径、用户、权限组 (角色) 。用户属于某个权限组,某个权限组拥有某些路径的访问权限。

  1. 初始化:获取整个项目的所有路径以方便后续分配权限。
  2. 配置权限:在配置页面,配置每个用户组能访问的路径,同时配置某个用户属于哪个权限组。
  3. 拦截请求:向springboot中添加interceptor,拦截请求。
  4. 检验权限:符合条件正常放行,不符合条件返回403 forbidden。

初始化

分为路径初始化和权限初始化两个步骤。路径初始化是指提取整个项目所有路径,并保存到MySQL数据库。

路径初始化

Springboot会把所有URL和Method(具体而言是RequestMapping注解的信息封装的RequestMappingInfo类)与JAVA类的对应关系保存到RequestMappingHandlerMapping对象中,通过这对象获取所有接口并保存到数据库中。

@PostConstruct
    private void initPaths(){
        log.info("path初始化开始");
        initPermissionGroups();
        // 生成版本号
        Long versionId = SnowflakeUtil.getId();
        // 新增预先设置的path
        initPrePaths(versionId);
        // 将管理员加入dev组中
        initAdmin();
        // 获取所有path
        Map<RequestMappingInfo, HandlerMethod> requestMappingInfoHandlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> m : requestMappingInfoHandlerMethodMap.entrySet()) {
            RequestMappingInfo info = m.getKey();
            HandlerMethod method = m.getValue();
            RequestMethodsRequestCondition methodsCondition = info.getMethodsCondition();
            if (methodsCondition.getMethods().isEmpty()){
                continue;
            }

            Paths entity = getPathByRequestMappingInfo(versionId, m);
            entity = pathsService.insertOrUpdate(entity);

            // 判断是否有默认组
            // 省略默认组相关逻辑,这里提供了一个注解,可以写在控制器类上,可以直接注明这个类默认是什么权限组的,方便配置。
        }
        // 查询所有旧path
        List<Paths> oldPaths = pathsService.queryAllOldPath(versionId);
        for (Paths oldPath:
                oldPaths) {
            pathsService.deleteByIdAndHandleGroupAndBlackList(oldPath.getId());
        }
        log.info("path初始化结束");
    }

权限初始化

这里实际上是一个优化措施,只初始化免登录与基础权限组(实质上是把相关内容从数据库搬到内存中),保存在PermissionMappingInfo类的变量里面,后续在检查这两个权限时,直接拿出这个Bean,获取变量然后检验就可以,速度可以非常快(因为不用访问数据库)。

这里的具体逻辑写在:

//对应Config类
@PostConstruct//在SpringBean全部构建完成后要做的操作(系统启动时的初始化操作)
    public void init() throws InterruptedException {
        log.info("权限初始化开始");
        permissionMappingInfo.init(requestMappingHandlerMapping);
        log.info("权限初始化完成");
    }

//PermissionMappingInfo类
public void init(RequestMappingHandlerMapping requestMappingHandlerMapping) throws InterruptedException {
        this.requestMappingHandlerMapping = requestMappingHandlerMapping;
        methodsPermissionGroupsNoNeedLogin = new ConcurrentHashMap<>();
        methodsPermissionGroupsOther = new ConcurrentHashMap<>();
        methodsPermissionGroupsIsBase = new ConcurrentHashMap<>();
        updateMappingInfoExecutor = new ThreadPoolExecutor(8,
                8,
                100,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        preInit();//函数内容是调用setAllMappingPermissionGroupsInfo()
        updateMappingInfoExecutor.execute(this::task);
    }

	//定时任务,每十秒刷新这两个组的内容
    private void task() {
        try {
            while (true) {
                setAllMappingPermissionGroupsInfo();//作用是读取数据库,把内容读取到内存中
                clearOldData();
                Thread.sleep(10000);
            }
        }catch (Exception e) {
            log.error("ERROR: ", e);
        }
    }

配置权限

这个比较简单,主要是写个接口更新数据库的值就好,简单的增删改查。具体内容就不写了,没什么意义。

拦截请求与检验权限

配置MvcConfigurer,添加拦截器LoginInterceptor()。

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {

            HandlerMethod method = (HandlerMethod) handler;// 把handler强转为HandlerMethod
            Method key = method.getMethod();

            // 是否在免登录组里
            if (checkNeedLogin(key)) {
                return true;
            }

            Users user = checkLogin(request);

            // TODO: 黑名单

            if (checkIsBase(key)) {
                return true;
            }


            // 查出该用户加入的非基础权限组 基础权限组 和 免登录组映射关系不在usersofGroup表中
            UsersOfGroup usersOfGroupParams = new UsersOfGroup();
            usersOfGroupParams.setUserId(user.getId());
            usersOfGroupParams.setIsEffective(1);
            List<UsersOfGroup> usersOfGroupRet = usersOfGroupService.queryAll(usersOfGroupParams);
            ConcurrentHashMap<Long, PermissionMappingInfo.PermissionMappingInfoEntry> map = permissionMappingInfo.getMethodsPermissionGroupsOther().get(key);
            for (UsersOfGroup ug:
                 usersOfGroupRet) {
                // 判断该用户加入组是否有效
                if (ug.getIsEffective() != 1 || !checkDate(ug.getStartAt(), ug.getEndAt())) {
                    // 一个有效即可
                    continue;
                }

                // 是否是dev组成员
                if (Objects.equals(ug.getGroupId(), PermissionGroupConstant.DEV_GROUP.getId())) {
                    return true;
                }

                if (map == null || map.isEmpty()) {
                    // 该方法没有加入到其他权限组
                    continue;
                }

                PermissionMappingInfo.PermissionMappingInfoEntry entry = map.get(ug.getGroupId());
                if (entry == null) {
                    continue;
                }

                if (checkSingleGroup(entry.getPermissionGroups(), entry.getPathsOfGroup())) {
					//简单检测date和isEffective后
                    return true;
                }
            }


            throw new ForbiddenException();
        }
        throw new ForbiddenException();
    }

这样就好了,对每个请求都检测。

License:  CC BY 4.0