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

news2024/9/22 5:38:16

一、背景

微服务开发中,暴露在外网的接口,为了访问的安全,都是需要在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};

}

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

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

public static class RoleType {
        /**
         * 学生
         */
        public static final String STUDENT = "student";

        /**
         * 老师
         */
        public static final String TEACHER = "teacher";
    }

三、EL表达式

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

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

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

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;
    }

四、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;
    }

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

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

五、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();
    }
}

六、总结

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

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

权限项的校验

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

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

// 避免每次都查库,可以适当缓存一定时间
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, "用户没有此操作的权限");
}

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1237951.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【word技巧】Word制作试卷,ABCD选项如何对齐?

使用word文件制作试卷&#xff0c;如何将ABCD选项全部设置对齐&#xff1f;除了一直按空格或者Tab键以外&#xff0c;还有其他方法吗&#xff1f;今天分享如何将ABCD选项对齐。 首先&#xff0c;我们打开【替换和查找】&#xff0c;在查找内容输入空格&#xff0c;然后点击全部…

渗透工具---BurpSuite 插件开发之HelloWorld

本文主要记录如何利用burp官方的新版API即MontoyaApi 写helloworld&#xff08;上一篇的demo使用旧版api写的&#xff0c;这篇及后续开发将采用新版api&#xff09; 先看效果图 更多详细内容见下方 这里有更详细更全面的代码内容 以及配置相关的内容 https://mp.weixin.qq.co…

面试题-8

1.vue路由是怎么传参的&#xff1f; params传参 this.$router.push({name:index}) this.$route.params.id 路由属性传参 this.$router.push({name:/index/${item.id}}) 配置路由{path:/index:id} query传参(可以解决页面刷新参数丢失的问题) this.$router.push({ name…

云原生入门系列(背景和驱动力)

做任何一件事&#xff0c;或者学习、应用一个领域的技术&#xff0c;莫过于先要想好阶段的目标和理解、学习它的意义是什么&#xff1f;解决了什么问题&#xff1f; 这部分&#xff0c;就尝试来探讨下这个阶段需要理解并达成的目标以及践行云原生的意义在哪里。 1.历程 任何阶…

海辰储能与FlexGen签署储能系统合作协议,加快拓展北美市场

海辰储能宣布与美国储能技术提供商和系统集成商FlexGen达成合作协议。根据协议&#xff0c;海辰储能将为FlexGen供应10GWh的先进储能产品&#xff1b;同时&#xff0c;其将支持FlexGen能源管理系统(EMS)用于总容量为15GWh的项目。 作为协议的一部分&#xff0c;FlexGen还将成为…

Windows 7 连接 Windows 10 共享打印机,Windows 无法连接打印机,操作失败,错误为0x0000011b 的终极解决办法

Windows 7 连接 Windows 10 共享打印机出现错误 0x000001b&#xff0c;建议不要通过卸载Windows10系统的KB5005565安全更新来解决该问题&#xff08;犹如削足适履&#xff09;&#xff0c;正确的处理方法是手工添加一个本地打印机&#xff0c;本方法是安全可靠的。本文详述了该…

双12电视盒子推荐:测评员解析目前电视盒子哪个最好

电视盒子不需要每月缴费&#xff0c;只需联网就可以收看海量视频资源&#xff0c;游戏、网课、投屏等功能让电视盒子的使用场景更丰富&#xff0c;我每年都会进行数十次电视盒子测评&#xff0c;本期要分享的是双十二电视盒子推荐&#xff0c;全面解析目前电视盒子哪个最好。 一…

【数据结构】链表的八种形态

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 链表的三大"性状" 一.带头链表和不带头链表 头指针与头结点的异同 头指针 头结点 二.循环链表和非循环链表 三.双向链表和单向链表 链表的八大形态 结语…

Amlogic方案遥控器配置(Android11)

配置路线 键值变化路径&#xff1a; ScanCode --> Keycode Lable --> KeyCode Layout --> KeyLable --> Keycode – > KeyEvent 文件映射路径&#xff1a; *.dtsi --> input-event-codes.h --> *.kl --> InputEventLable.h --> kecodes.h --> P…

laravel引入element-ui后,blade模板中使用elementui时,事件未生效问题(下载element-ui到本地直接引入项目)

背景 重构公司后台项目&#xff0c;使用了dcat-admin&#xff0c;但是dcat-admin有些前端功能不能满足需求。因此引入element-ui进行相关界面的优化 具体流程 1.下载element-ui到本地 2.进入如下目录 打开 node_modules\element-ui\lib 复制index.js 打开 node_modules/ele…

UE4基础篇十五:AI行为树

一、学习完教程后需要掌握知识点 1.1、基础概念: 1.1 行为树:控制并显示AI的决策制定过程 1.2 黑板:可以看做是行为树的创建一些公有变量,外部可以修改行为树黑板的变量值,达到修改行为树状态的逻辑 1.3 环境查询: 获取地图环境中的信息进行一个筛选,查找到所需要的的…

微信公众号上怎么做微信助力活动

微信公众号微信助力活动&#xff1a;一起分享&#xff0c;一起成长 在当今社交媒体时代&#xff0c;人们之间的互动和分享变得越来越重要。微信公众号作为一个广受欢迎的社交平台&#xff0c;为人们提供了一个便捷的分享和交流渠道。而微信助力活动则是通过分享和互动&#xf…

【算法萌新闯力扣】:两句话中的不常见单词

力扣热题&#xff1a;两句话中的不常见单词 开篇 今天是备战蓝桥杯的第19天&#xff0c;今天到目前刷了4道力扣算法题。其中&#xff0c;这道题是对我来说收获最大的一道&#xff0c;让我更熟练地掌握了一些算法题中方法&#xff0c;于是来与大家分享一下。 题目链接: 884.两…

听说90%的人都没搞定手撕协程池这道面试题!

特别的缘分 听说90%的人都没搞定手撕协程池这道面试题&#xff01; 能看到这篇文章一定是特殊的缘分&#xff0c;请务必珍惜&#xff0c;请详细看看吧&#xff0c;哈哈。 不止上图&#xff0c;最近 Go就业训练营 中不少小伙伴说&#xff0c;面试中碰到了好几次手撕协程池的问题…

向上转型 向下转型 重写 多态 ---java

目录 一. 向上转型 1.1 概念 1.2 语法格式 1.3 动态绑定引入 1.4 重写的引入 1.5向上转型的使用方式 方式一: 直接赋值 方式二: 通过传参,进行向上转型(多态引入) 方法三:通过返回值, 进行向上转型 二. 重写 2.1 概念 2.2 重写的格式 2.3 重写的规则 【重写和重…

QT mysql 数据库线程池 与数据库操作封装

最近事情比较多很久没有写学习笔记了&#xff0c;数据库线程池&#xff0c; 数据库封装&#xff0c;虽说数据库操作有很多不需要写sql 的&#xff0c;ORM 封装的方式去操作数据库。但是从业这些年一直是自己动手写sql &#xff0c;还是改不了这个习惯。不说了直接上代码。 数据…

YOLOv5 环境搭建

YOLOv5 环境搭建 flyfish 环境 Ubuntu20.04 驱动、CUDA Toolkit、cuDNN、PyTorch版本对应 1 NVIDIA驱动安装 在[附加驱动界]面安装驱动时&#xff0c;需要输入安全密码&#xff0c;需要记下&#xff0c;后面还需要输入这个密码 重启之后有的机器会出现 perform mok manage…

【Java开发】 Springboot集成Mybatis-Flex

1 Mybatis-Flex 介绍 1.1简介 Mybatis-Flex 是一个优雅的 Mybatis 增强框架&#xff0c;它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库&#xff0c;其内置的 QueryWrapper 亮点帮助我们极大的减少了 SQL 编写的工作的同时&#xff…

微信订房功能怎么做_公众号里怎么实现在线订房系统

微信公众号在线订房系统&#xff1a;一键解决您的住宿问题 在当今数字化时代&#xff0c;微信公众号已经成为人们生活中不可或缺的一部分。它提供了各种各样的功能和服务&#xff0c;让我们的生活变得更加便捷和高效。而如今&#xff0c;微信公众号也实现了在线订房功能&#…

SecureCRT -- 使用说明

【概念解释】什么是SSH&#xff1f; SSH的英文全称是Secure Shell 传统的网络服务程序&#xff0c;如&#xff1a;ftp和telnet在本质上都是不安全的&#xff0c;因为它们在网络上用明文传送口令和数据&#xff0c;别有用心的人非常容易就可以截获这些口令和数据。而通过使用SS…