• 微服务开发中,使用AOP和自定义注解实现对权限的校验


    一、背景

    微服务开发中,暴露在外网的接口,为了访问的安全,都是需要在http请求中传入登录时颁发的token。这时候,我们需要有专门用来做校验token并解析用户信息的服务。如下图所示,http请求先经过api网关,网关会去调用认证服务进行token解析(因为token是认证服务所颁发),反解析出token中包含的用户信息,最后经过http header透传给业务服务(供业务服务直接使用)。

    在这里插入图片描述

    本文主要是描述业务服务中,如何对api网关透传过来的报文进行权限的校验。

    这里重申一下,建议每个服务自己去实现权限的校验。虽然工作量有的时候会重复,但是适用于中小公司没有统一权限管理的实际情况。

    本文会涉及到的几个知识点:

    • AOP切面编程
    • 自定义注解

    二、自定义注解

    • 权限开关
    • 用户ID,需读取注解所在方法的入参值
    • 角色列表,限定方法访问所需的角色列表,这里默认是教师-teacher,就是说登录用户的角色必须含有教师角色。
    import java.lang.annotation.*;
    
    /**
     * 权限限制.
     *
     * @author xxx
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface PermissionLimit {
    
        /**
         * 权限校验(默认true)
         */
        boolean limit() default true;
    
        /**
         * 入参-用户ID
         *
         * @return
         */
        String userId();
    
        /**
         * 角色列表(默认teacher-教师)
         *
         * @return
         */
        String[] roles() default {Constants.RoleType.TEACHER};
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    允许访问的角色列表,这里使用数组的方式, 因为一个用户可能有多个角色,而一个方法也可能被多个角色所允许访问。

    本系统为了简单讲解,角色只有以下2个:

    public static class RoleType {
            /**
             * 学生
             */
            public static final String STUDENT = "student";
    
            /**
             * 老师
             */
            public static final String TEACHER = "teacher";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    三、EL表达式

    使用@Aspect对自定义注解PermissionLimit进行拦截,读取注解中的userId,和透传参数进行对比。

    要读取注解中的userId,就需要支持el表达式,可能有下面两种情况:

    • 对象.属性
        @PostMapping("/order/copy")
        @PermissionLimit(userId = "#request.userId")
        public ResponseEntity<?> copy(@Validated @RequestBody OrderCopyRequest request) {
        }
    
    • 1
    • 2
    • 3
    • 4
    • 变量
        @PostMapping("/order/create")
        @PermissionLimit(userId = "#userId")
        public ResponseEntity<?> create(@RequestParam Long userId) {
        }
    
    • 1
    • 2
    • 3
    • 4

    Java中有对el表达式支持解析:

    import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
    import org.springframework.expression.EvaluationContext;
    import org.springframework.expression.Expression;
    import org.springframework.expression.ExpressionParser;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.expression.spel.support.StandardEvaluationContext;
    
        private final ExpressionParser expressionParser = new SpelExpressionParser();
    
        private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        
        // elExpression 即#request.userId 或者 #userId
        // method 注解所在的方法
        // args 方法的参数值
        private Object evaluateExpression(String elExpression, Method method, Object[] args) {
            Expression expression = expressionParser.parseExpression(elExpression);
    
            EvaluationContext context = this.bindParam(method, args);
    
            return expression.getValue(context);
        }
    
        private EvaluationContext bindParam(Method method, Object[] args) {
            // 获取方法的参数名
            String[] params = discoverer.getParameterNames(method);
    
            EvaluationContext context = new StandardEvaluationContext();
    
            for (int i = 0; i < params.length; i++) {
                // 把方法的参数值赋给EvaluationContext
                context.setVariable(params[i], args[i]);
            }
    
            return context;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    四、HttpServletRequest

    自定义注解只能修饰controller层的方法,它需要读取http header的透传字段。
    所以,前提是获得HttpServletRequest对象,具体语句见下:

    import org.springframework.web.context.request.RequestAttributes;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
        private HttpServletRequest getRequest() {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            if (requestAttributes instanceof ServletRequestAttributes) {
                return ((ServletRequestAttributes) requestAttributes).getRequest();
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    接下来,读取http header中的透传字段userId,实现语句如下:

       HttpServletRequest request = this.getRequest();
       if (null != request) {
           //2.当前登录用户的userId
           final String authUserId = request.getHeader(JwtAuthHeaders.AUTH_USER_ID);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    五、AOP切面

    • PermissionLimit是我们的自定义注解
    @Component
    @Aspect
    public class PermissionAspect {
        @Autowired
        private CommonConfig commonConfig;
    
        @Pointcut("@annotation(permissionLimit)")
        public void pointcut(PermissionLimit permissionLimit) {
    
        }
    
        @Around("pointcut(permissionLimit)")
        public Object around(ProceedingJoinPoint joinPoint, PermissionLimit permissionLimit) throws Throwable {
            // 1.开关是否开启(全局开关和注解的开关)
            if (!commonConfig.getEnabledPermission() || !permissionLimit.limit()) {
                return joinPoint.proceed();
            }
    
            Method method = this.getMethod(joinPoint);
            Object[] args = joinPoint.getArgs();
    
            HttpServletRequest request = this.getRequest();
            if (null != request) {
                //2.从token中解析出当前登录用户的userId
                final String authUserIdStr = request.getHeader(JwtAuthHeaders.AUTH_USER_ID);
                Precondition.isTrue(StrUtil.isNotBlank(authUserIdStr), "用户未登录");
    
                //3.是否一致
                String userId = this.evaluateExpression(permissionLimit.userId(), method, args).toString();
                Precondition.isTrue(authUserIdStr.equals(userId), "用户不一致");
    
                //4.角色校验
                final String userRoles = request.getHeader(JwtAuthHeaders.AUTH_USER_ROLE);
                Precondition.isTrue(StrUtil.isNotBlank(userRoles), "未获取到登录用户的角色");
    
                String[] authorityRoleArray = permissionLimit.roles();
                Set<String> authorityRoleSet = Arrays.stream(authorityRoleArray).collect(Collectors.toSet());
    
                if (!CollectionUtils.isEmpty(authorityRoleSet)) {
                    boolean hasAuthority = false;
    
                    String[] userRoleArray = userRoles.split(",");
    
                    for (String role : userRoleArray) {
                        // 用户的任意一个角色被包含在里面,则说明拥有此方法的权限
                        hasAuthority = authorityRoleSet.contains(role);
                        if (hasAuthority) {
                            break;
                        }
                    }
                    Precondition.isTrue(hasAuthority, "用户没有此操作的权限");
                }
            }
    
            return joinPoint.proceed();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    六、总结

    本文总结下整个的权限校验流程:

    • 全局开关, 是针对整个项目而言,在不同的环境下,开或关,方便调试。(如果是本地就需要关闭,而生产环境才打开。)
    • 方法开关,多少有点鸡肋了,好在它有默认值,不会增加你使用的复杂度。
      在这里插入图片描述

    权限项的校验

    本文实现了角色的校验,如果要细到权限项的话,需要查询业务服务中用户配置的权限项列表。

    下面仅给出其伪代码实现,以供参考。

    // 避免每次都查库,可以适当缓存一定时间
    String[] authorityArray = permissionLimit.authority();
    Set<String> authoritySet = Arrays.stream(authorityArray).collect(Collectors.toSet());
    
    if (!CollectionUtils.isEmpty(authorityRoleSet)) {
        boolean hasAuthority = false;
    
        List<String> authorities = userService.getUser(userId);
    
        for (String authority : authorities) {
            // 用户的任意一个权限项被包含在里面,则说明拥有此方法的权限
            hasAuthority = authoritySet.contains(authority);
            if (hasAuthority) {
                break;
            }
        }
        Precondition.isTrue(hasAuthority, "用户没有此操作的权限");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以说, 它的实现和角色的校验如出一辙,不同的是,往往权限项会更细致,也就是比角色的记录数更多罢了。

    如果你采用的是权限项的校验,而非角色,那么请减少每次的查库操作,可以对缓存做一个恰当有效期。

  • 相关阅读:
    展会预告 | 图扑邀您共聚 IOTE 国际物联网展·深圳站
    pytorch+sklearn实现数据加载
    Docker容器端口在主机的映射
    springboot 链接doris 配置
    责任链模式
    java程序中为什么经常使用tomcat
    【线代】矩阵的秩
    【线性代数/机器学习】矩阵的奇异值与奇异值分解(SVD)
    「区块链+数字身份」:DID 身份认证的新战场
    Node服务端框架Express-Sequelize-Mysql模型架构设计
  • 原文地址:https://blog.csdn.net/zhuganlai168/article/details/134552041