微服务实战(六):角色权限控制

这一节聊一下如何自己实现角色、权限、菜单控制。

思考:如何在访问接口的时候进行角色判断和权限校验

首先,我们已经有了角色、菜单、权限的管理,比如创建一个角色admin,创建很多个权限指令,如user-service:user/saveOrUpdate、user-service:user/delete等等,创建一些菜单,将菜单和权限指令分配到角色上,表示某个角色可以看到哪些菜单页面,拥有哪些操作指令,这些权限指令可以更细化,前端可以控制是否显示等,后端接口可以进行访问校验,当某个用户访问接口时,获取该用户的角色权限进行匹配,不匹配则不能访问接口。

那么,如何在用户访问接口的时候,来判断呢,也就是必须有某些指令或角色时,才能访问该接口,比较容易想到使用注解的方式,切面AOP来进行控制

实现角色、权限指令控制

先来看一下使用方法:

    @Permit(hasRoles = {"admin"}, hasPermissions = {"user-center:user:delete"})
    @ApiOperation("删除用户")
    @DeleteMapping("delete/{username}")
    public JsonData delete(@PathVariable("username") String username) {
        try {
            userService.delete(username);
            return JsonData.buildSuccess();
        } catch (Exception e) {
            return JsonData.buildError("删除失败:" + e.getMessage());
        }
    }

@Permit(hasRoles = {"admin"}, hasPermissions = {"user-center:user:delete"}) 注解,表示必须当前用户必须拥有admin这个角色,并且拥有这个指令才能访问该接口

1、创建注解接口

在common包中

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permit {

    String[] hasRoles() default {};

    String[] hasPermissions() default {};

}

hasRoles和hasPermissions两个参数,都是数组,有多个角色或权限指令

2、创建切面类

在common包中

@Slf4j
@Aspect
@Configuration
@SuppressWarnings({"unused"})
public class PermitAspect {

    @Resource
    private RedisTemplate<String, BoundHashOperations> redisTemplate;

    @Pointcut("@annotation(net.work.annotation.Permit)")
    public void annotationPointcut() {

    }

    @Before("annotationPointcut()")
    public void beforePointcut(JoinPoint joinPoint) {
        // 此处进入到方法前  可以实现一些业务逻辑

    }

    /**
     * 角色、权限校验
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("annotationPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取当前登录的用户信息
        LoginUser loginUser = LoginInterceptor.threadLocal.get();
        // 获取注解上的参数值
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Permit permit = method.getAnnotation(Permit.class);

        String[] roles = permit.hasRoles();
        String[] permissions = permit.hasPermissions();

        int userId = loginUser.getId();
        BoundHashOperations<String, String, Object> permitOps = getPermitOps(userId);

        if (roles.length > 0) {
            // 判断缓存中是否存在角色
            for (String role : roles) {
                List<String> cacheRole = (List<String>) permitOps.get(role);
                if (cacheRole == null) {
                    log.error("无操作权限!");
                    return JsonData.buildResult(BizCodeEnum.ACCOUNT_NO_PERMIT);
                }

                // 判断是否有指令权限
                if (!cacheRole.containsAll(Arrays.asList(permissions))) {
                    log.error("无操作权限!");
                    return JsonData.buildResult(BizCodeEnum.ACCOUNT_NO_PERMIT);
                }
            }
        } else {
            // 没有指定角色,但是需要指令权限
            if (permissions.length > 0) {
                Map<String, Object> cacheRole = permitOps.entries();
                if (cacheRole != null) {
                    Set<String> rolesKey = cacheRole.keySet();
                    Set<String> permissionList = new HashSet<>();
                    for (String roleKey : rolesKey) {
                        permissionList.addAll((Collection<? extends String>) cacheRole.get(roleKey));
                    }

                    if (!permissionList.containsAll(Arrays.asList(permissions))) {
                        log.error("无操作权限!");
                        return JsonData.buildResult(BizCodeEnum.ACCOUNT_NO_PERMIT);
                    }
                }
            }
        }

        return joinPoint.proceed();
    }

    /**
     * 在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
     *
     * @param joinPoint
     */
    @AfterReturning("annotationPointcut()")
    public void doAfterReturning(JoinPoint joinPoint) {

    }


    private BoundHashOperations<String, String, Object> getPermitOps(int userId) {
        return redisTemplate.boundHashOps(CacheConst.PERMIT_KEY + userId);
    }

}

这里的思路就是从redis中获取用户的角色和权限指令,然后与注解上配置的进行匹配。那么redis中用户的角色权限信息是什么时候存进去呢?这里需要考虑什么操作会影响用户的角色权限,并且能马上生效:1.对用户进行角色的增、删、改   2.对角色指令进行增、删、改,这里并没有在这些接口实现中去处理,而是使用注解来进行同步修改

用户角色、权限的同步

先来看一下使用方法,访问这个接口后,就会进行角色权限信息同步了,同步到redis中

    @SyncPermitCache
    @Permit(hasRoles = {"admin"})
    @ApiOperation("用户分配角色")
    @PostMapping("addUserRoles")
    public JsonData addUserRoles(@RequestBody UserRoleReq request) {
        try {
            userService.addUserRoles(request);
            return JsonData.buildSuccess();
        } catch (Exception e) {
            return JsonData.buildError("操作失败:" + e.getMessage());
        }
    }

1、创建注解接口

在common包中,可以看上面的图

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SyncPermitCache {

    boolean sync() default true;

}

sync表示是否开启同步,默认肯定是开启的

2、创建切面类

在user-center服务中

@Slf4j
@Aspect
@Configuration
@SuppressWarnings({"unused"})
public class SyncPermitCacheAspect {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    @Autowired
    private UserMapper userMapper;

    @Resource
    private RedisTemplate<String, BoundHashOperations> redisTemplate;

    @Pointcut("@annotation(net.work.annotation.SyncPermitCache)")
    public void annotationPointcut() {

    }

    @Before("annotationPointcut()")
    public void beforePointcut(JoinPoint joinPoint) {
        // 此处进入到方法前  可以实现一些业务逻辑

    }

    @Around("annotationPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    /**
     * 同步角色、权限到缓存
     *
     * @param joinPoint
     */
    @AfterReturning("annotationPointcut()")
    public void doAfterReturning(JoinPoint joinPoint) {
        // 获取注解上的参数值
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        SyncPermitCache syncPermitCache = method.getAnnotation(SyncPermitCache.class);

        if (syncPermitCache.sync()) {
            log.info("角色权限信息变更,开始同步...");

            Set<String> cacheKeys = redisTemplate.keys(CacheConst.PERMIT_KEY + "*");
            if (cacheKeys != null) {
                redisTemplate.delete(cacheKeys);
            }
            List<SysUserDO> sysUserDOList = userMapper.selectList(null);
            sysUserDOList.forEach(sysUserDO -> {
                BoundHashOperations<String, String, Object> permitOps = getPermitOps(sysUserDO.getId());
                // 查询当前用户的角色列表
                List<SysRoleDO> sysRoleDOList = roleMapper.findRoleListByUserId(sysUserDO.getId());
                sysRoleDOList.forEach(roleDO -> {
                    Set<String> permissions = new HashSet<>();
                    List<SysPermissionDO> sysPermissionDOList = permissionMapper.findPermissionListByRoleId(roleDO.getId());
                    if (sysPermissionDOList.size() == 0) {
                        permitOps.put(roleDO.getCode(), new HashSet<>());
                    } else {
                        sysPermissionDOList.forEach(permissionDO -> permissions.add(permissionDO.getPermission()));
                        permitOps.put(roleDO.getCode(), permissions);
                    }
                });
            });

            log.info("同步完成.");
        }
    }


    private BoundHashOperations<String, String, Object> getPermitOps(int userId) {
        return redisTemplate.boundHashOps(CacheConst.PERMIT_KEY + userId);
    }

}

注意这里是在doAfterReturning方法中实现,跟@Permit不一样。

在相关操作接口上加上这个同步注解后,就能实现用户角色权限变更时,同步到redis。用户不需要重新登录。

还有一个考虑的点就是服务每次重启时需要同步一次数据表中的所有用户角色权限信息到redis

在user-center中创建config类,这样启动的时候就会同步一次数据了

/**
 * 初始化用户角色权限同步到redis
 */
@Component
public class InitUserSyncConfig implements ApplicationListener<ContextRefreshedEvent> {

    @SyncPermitCache
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {

    }
}

到这里用户服务结束,后面可自行在business-center中增加自己的业务服务模块


版权声明:本文为u014608435原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。