微服务开发中,使用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};

}

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

本系统为了简单讲解,角色只有以下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.mfbz.cn/a/176215.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

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

【HarmonyOS】API6上JS实现视频播放全屏播放时,会回到之前界面

【关键字】 API6 / 视频播放 / 全屏播放异常 【问题现象】 开发者在API6上用JS实现视频播放器点全屏播放后&#xff0c;不是全屏效果&#xff0c;实际效果是变成了横屏并返回到首页。 具体代码实现是参考video媒体组件指南。 【问题分析】 JS实现视频播放器有Codelab代码示…

基于springboot实现乒乓球预约管理系统项目【项目源码】计算机毕业设计

基于springboot实现乒乓球预约管理系统演示 系统的开发环境 浏览器&#xff1a;IE 8.1&#xff08;推荐6.0以上&#xff09; 开发使用语言&#xff1a;JAVA JDK版本&#xff1a;JDK_8 数据库管理系统软件&#xff1a;Mysql 运行平台&#xff1a;Windows 7 运行环境&#…

HarmonyOS ArkTS语言,运行Hello World(二)

一、认识DevEco Studio界面 进入IDE后&#xff0c;我们首先了解一下基础的界面。整个IDE的界面大致上可以分为四个部分&#xff0c;分别是代码编辑区、通知栏、工程目录区以及预览区。 代码编辑区 1、中间的是代码编辑区&#xff0c;你可以在这里修改你的代码&#xff0c;以…

CRMEB Pro版 v3.0详情预告(附件crmebPro功能思维导图)

首先&#xff0c;先来看看本次CRMEB Pro版 v3.0 的整体升级框架 翩若惊鸿 CRMEB Pro版 从设计之初&#xff0c;就十分重视用户体验&#xff0c;在保证强大功能的同时&#xff0c;本次也为大家带来了领先于业界的UI 3.0&#xff0c;一目惊鸿。 一、风格升级 1、圆角风格 商城…

轻松整理文件夹,将视频文件全部归类到另一个文件夹!

如果你需要整理文件夹中的文件&#xff0c;将同一类别的文件归纳到一起&#xff0c;可以更加方便地管理和查找。现在&#xff0c;我们有一个简单而实用的方法&#xff0c;可以将文件夹中的所有视频文件归类到另一个文件夹中&#xff0c;让你的文件管理更加有序和高效。 首先&am…

动能方案 | 15693协议的读卡器应用 DP1363F 替代RC663

15693协议是一种高频&#xff08;13.56 MHz&#xff09;射频识别&#xff08;RFID&#xff09;协议&#xff0c;广泛满足无线识别和数据传输领域。其特点包括较远的读取范围、支持快速数据传输、与多个标签的兼容等&#xff0c;产生于不同行业有着广泛的应用&#xff0c;包括但…

10个即时通讯软件开发项目经验教训

即时通讯软件开发在现代社交和商务交流中扮演着重要的角色。然而&#xff0c;这个领域也充满了挑战。在本文中&#xff0c;我将探讨即时通讯软件开发的重要性以及开发者面临的挑战&#xff0c;并分享一些应对策略。 10个经验教训 明确需求&#xff1a;在开始开发之前&#xf…

CRM中线索的概念和使用技巧

CRM中线索是什么&#xff1f;如何管理线索&#xff1f;CRM系统中线索通常指通过展会、线上、广告等方式获取到的原始客户信息。这些潜在的客户信息经过市场培育、SDR筛选&#xff0c;进而成为一个合格商机。下面我们从3个方面介绍什么是线索管理。 1.线索来源 线索来源渠道非…

来吧,SpringBoot的自动配置原理都在这里了

&#x1f497;推荐阅读文章&#x1f497; &#x1f338;JavaSE系列&#x1f338;&#x1f449;1️⃣《JavaSE系列教程》&#x1f33a;MySQL系列&#x1f33a;&#x1f449;2️⃣《MySQL系列教程》&#x1f340;JavaWeb系列&#x1f340;&#x1f449;3️⃣《JavaWeb系列教程》…

ELK企业级日志分析平台

目录 一、elasticsearch 1、集群部署 2、cerebro部署 3、elasticsearch-head插件部署 4、elasticsearch集群角色分类 二、logstash 1、部署 2、elasticsearch输出插件 3、file输入插件 4、file输出插件 5、syslog 插件 6、多行过滤插件 7、grok过滤 三、kibana数…

vr小鼠虚拟解剖实验教学平台减少了受感染风险

家畜解剖实验教学是培养畜牧兽医专业学生实际操作能力的专业教学活动中的核心手段。采取新型教学方式与手段&#xff0c;合理设置实验教学内容&#xff0c;有助于激发学生的操作积极性&#xff0c;促进实践教学的改革。 家畜解剖VR仿真教学是一种借助VR虚拟现实制作和web3d开发…

谷歌Freshness新鲜度算法:如何利用它提升网站排名?

今天我们就来深入了解下Google Freshness算法核心&#xff0c;结合案例研究和实用技巧&#xff0c;为我们自己的网站优化提供一些思路。 Google新鲜度算法和QDF Google的新鲜度算法和查询需求的新鲜度&#xff08;Query Deserves Freshness, QDF&#xff09;模型是为了改善特…

爱创科技总裁谢朝晖荣获“推动医药健康产业高质量发展人物”

中国医药市场规模已经成为全球第二大医药市场&#xff0c;仅次于美国。近年来&#xff0c;随着中国经济的持续增长和人民生活水平的提高&#xff0c;医药市场需求不断扩大。政府对医疗卫生事业的投入也在不断加大&#xff0c;为医药行业的发展创造了良好的政策环境。为推动医药…

和田2023年群众舞蹈大赛总决赛圆满落幕!

11月19日&#xff0c;由中共和田地委宣传部主办&#xff0c;地区文旅局承办&#xff0c;地区文化馆、各县市文旅局协办&#xff0c;北京市援疆和田指挥部支持的和田地区2023年“大地欢歌 舞动和田”群众舞蹈大赛总决赛在和田市新夜市圆满落幕&#xff0c;比赛最终决出一等奖1名…

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…

实例讲解Simulink的MATLAB Function模块

内容 MATLAB Function是一个支持使用M语言编写模块功能,并能够将所编写的M语言生成C代码&#xff0c;用于开发桌面应用和嵌入式应用的模块。它支持的 MATLAB内建函数比 Fcn模块要广泛&#xff0c;除去基本的四则运算、逻辑操作符和关系操作符&#xff0c;还可以调用MATLAB各种…

许战海战略文库|从丰田到等离子屏:技术领先为何失去市场?

引言&#xff1a;在探讨技术创新与市场需求之间的微妙关系时&#xff0c;个关键的问题浮现:为什么强大的技术优势并不总是等同于市场成功?从丰田汽车在电动车领域的挑战到日本等离子显示屏技术的衰落,市场趋势对企业成功存在决定性影响。企业需要在技术创新和市场需求之间找到…

在有springSecurity或者若依项目中获取当前系统登录的用户信息

方法一&#xff08;springSecurity自带的&#xff09; AuthenticationPrincipal 是 Spring Security 框架中的一个注解&#xff0c;用于获取当前已认证用户的 principal&#xff08;即用户身份信息&#xff09;。 方法二&#xff08;若依项目自带的&#xff09; &#xff08;1…

基于C#实现Bitmap算法

在所有具有性能优化的数据结构中&#xff0c;我想大家使用最多的就是 hash 表&#xff0c;是的&#xff0c;在具有定位查找上具有 O(1)的常量时间&#xff0c;多么的简洁优美&#xff0c;但是在特定的场合下&#xff1a; ①&#xff1a;对 10 亿个不重复的整数进行排序。 ②&am…