微服务开发中,使用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++) {// 把方法的参数值赋给EvaluationContextcontext.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.当前登录用户的userIdfinal String authUserId = request.getHeader(JwtAuthHeaders.AUTH_USER_ID);}

五、AOP切面

  • PermissionLimit是我们的自定义注解
@Component
@Aspect
public class PermissionAspect {@Autowiredprivate 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中解析出当前登录用户的userIdfinal 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.hqwc.cn/news/205913.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云原生正在重塑软件的整个生命周期(内附资料)

随着企业数字化转型进程的发展&#xff0c;企业面临着新旧商业形态的剧变&#xff0c;颠覆和重构时刻都在发生。 企业需要更加快速地感知用户侧的需求变化并做出调整&#xff0c;才有可能在竞争中持续积累优势。业务的个性化、敏捷化、智能化需求日益突显&#xff0c;数字化应…

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题 文章目录 【Python】Vscode解决Python中制表符和空格混用导致的缩进问题1. 问题来源2. 解决Reference 1. 问题来源 在python中使用缩进来进行代码块的分区&#xff0c;通常来说python的一个缩进包含4个空格&#…

[Linux] shell条件语句和if语句

一、条件语句 1.1 测试 test 测试文件的表达式是否成立 格式&#xff1a;test 条件表达式 [ 条件表达式 ] 选项作用-d测试是否为目录-e测试目录或文件是否存在-a测试目录或文件是否存在-f测试是否为文件-r测试当前用户是否有权限读取-w测试当前用户是否有权限写入-x测试当前…

UE4基础篇十五:AI行为树

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

ZLMediaKit安装配置和推拉流

一、ZLMediaKit 库简介 ZLMediaKit 是一个基于 C11 的高性能运营级流媒体服务框架 官方写的项目特点&#xff1a; 基于 C11 开发&#xff0c;避免使用裸指针&#xff0c;代码稳定可靠&#xff0c;性能优越。 支持多种协议(RTSP/RTMP/HLS/HTTP-FLV/Websocket-FLV/GB28181/MP…

法大大携手广西数通科技,助推金融行业数字化变革

自2017年《关于积极推进供应链创新与应用的指导意见》首次对供应链创新发展作出重要部署以来&#xff0c;我国供应链金融业务实现了有效创新发展。数据显示&#xff0c;2022年&#xff0c;我国供应链金融数字化规模达到11万亿元&#xff0c;数字化渗透率约为30%&#xff0c;相比…

思福迪 运维安全管理系统 test_qrcode_b 远程命令执行漏洞

思福迪 运维安全管理系统 test_qrcode_b 远程命令执行漏洞 一、漏洞描述二、漏洞影响三、网络测绘四、漏洞复现1.手动复现2.自动化复现3.python源代码 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任…

看图说话:对脏读、不可重复度、幻读进行总结

1、脏读 「事务B」将 id 为 1 的用户 name 修改为“小卡”&#xff0c;事务未提交。「事务A」查询 id 为 1 的用户数据&#xff0c;此时 name 已为“小卡”。 2、不可重复度 「事务A」第一次读取 id 为 1 的用户&#xff0c;name 是 “卡卡”。「事务B」将 id 为 1 的用户 nam…

聚焦数字化项目管理——2023年PMI项目管理大会亮点回顾

11月18日-19日&#xff0c;由PMI&#xff08;中国&#xff09;主办的2023年PMI项目管理大会在上海浦东嘉里大酒店圆满召开。本次大会以“数智时代&#xff0c;汇创未来”为主题&#xff0c;聚焦数智时代大背景下的项目管理行业发展和人才培养&#xff0c;吸引了海内外千余名项目…

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

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

python 水质日历热力图

利用日历热力图可以方便的查看站点水质全年的变化情况。 接口获取站点数据 这一步根据自己实际情况&#xff0c;也可以读取excel、MySQL读取数据。这里把API地址已隐去。 import numpy as np import calendar import requests import json import pandas as pd import time f…

9.4 Windows驱动开发:内核PE结构VA与FOA转换

本章将继续探索内核中解析PE文件的相关内容&#xff0c;PE文件中FOA与VA,RVA之间的转换也是很重要的&#xff0c;所谓的FOA是文件中的地址&#xff0c;VA则是内存装入后的虚拟地址&#xff0c;RVA是内存基址与当前地址的相对偏移&#xff0c;本章还是需要用到《内核解析PE结构导…