当前位置: 首页 > news >正文

做一个RBAC权限

在分布式应用场景下,我们可以利用网关对请求进行集中处理,实现了低耦合,高内聚的特性。

登陆权限验证和鉴权的功能都可以在网关层面进行处理:

  • 用户登录后签署的jwt保存在header中,用户信息则保存在redis中
  • 网关应该对不需要登录即可查看的网页请求放行,即需要一个白名单存放放行请求路径
  • gateway 依赖包已经包含了webflux组件,能够有效利用线程资源,提高效率,减少不必要的阻塞时间
  • spring-cloud-starter-loadbalancer 通过服务名调用并自动轮询实例,可以在后端代码(WebClient + @LoadBalanced)中向其他服务请求数据库数据
  • 负载均衡就是把大量请求按照某种算法分摊到多个后端实例上,以提高吞吐量、避免单点热点,并在实例挂掉时自动剔除。

在这里插入图片描述
从上至下:
定义一个日志记录器,用于打印日志信息。
从配置文件中读取 JWT 的密钥。
Jackson 提供的 工具类,用于序列化/反序列化 JSON。
注入你自定义的网关配置类(GatewayConfig)。
用于发起 HTTP 请求,特别是微服务之间的调用。
从配置文件中读取某个网关地址或标识。

public class AuthFilter implements GlobalFilter,先要实现这个接口

先看几个辅助函数:

1、writeJson : 渲染json数据到前端,这里用来将错误情况下的信息返回给前端,封装了状态码,响应头等数据

private Mono<Void> writeJson(ServerHttpResponse resp, HttpStatus status, ResponseResult jsonResult) {resp.setStatusCode(status);//401状态码resp.getHeaders().add("Content-Type", "application/json;charset=utf-8");try {String json = objectMapper.writeValueAsString(jsonResult);DataBuffer db = resp.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));return resp.writeWith(Mono.just(db));} catch (JsonProcessingException ex) {throw new RuntimeException("json序列化异常", ex);}}

这 6 行代码就是 WebFlux 版“返回 JSON 响应”的模板:
设状态码 → 设头 → 序列化 → 包成 Netty 缓冲区 → 异步写出 → 异常兜底。

这里展示一下Mono< T>的区别

对比维度Mono<String>Mono<Void>
语义0~1 个字符串0 个数据(只发 onComplete/onError
能拿到值吗?可以 block() / subscribe(s -> …) 拿到具体字符串拿不到任何值;只能知道“事件结束”或“出错”
典型用途查数据库、调接口、读文件……有返回体写响应、删数据、发消息……只关心成功/失败
序列化内容会把字符串写出去没有 body,只能写状态码/头
实际发出的 HTTP 包有 Content-Length > 0 的 bodybody 长度为 0(只有响应行+头)

2、Java的record

private record UriPattern(String[] allowedMethods, String pattern) {//判断是否匹配某个请求类型private boolean matchMethod(String method) {for (String am : allowedMethods) {if (am.equalsIgnoreCase(method) || "*".equals(am)) {return true;}}return false;}private static UriPattern of(String uri) {String[] parts = uri.split(":");if (parts.length == 1) {return new UriPattern(new String[]{"*"}, parts[0]);} else {return new UriPattern(parts[0].split(","), parts[1]);}}}

等效于下面的类:

import java.util.Arrays;
import java.util.Objects;public final class UriPattern {/* 1. 字段 */private final String[] allowedMethods;private final String pattern;/* 2. 全参构造器 */public UriPattern(String[] allowedMethods, String pattern) {// 防御性复制,防止外部数组被修改this.allowedMethods = Arrays.copyOf(allowedMethods, allowedMethods.length);this.pattern = pattern;}/* 3. 业务方法:判断方法是否允许 */public boolean matchMethod(String method) {for (String am : allowedMethods) {if (am.equalsIgnoreCase(method) || "*".equals(am)) {return true;}}return false;}/* 4. 静态工厂:解析配置串  "GET,POST:/api/user/**"  */public static UriPattern of(String uri) {String[] parts = uri.split(":");if (parts.length == 1) {// 只有 URI 模式,方法默认通配return new UriPattern(new String[]{"*"}, parts[0]);} else {// parts[0] 是方法列表,parts[1] 是 URI 模式return new UriPattern(parts[0].split(","), parts[1]);}}/* 5. getter(可选,方便外部读取) */public String[] getAllowedMethods() {return Arrays.copyOf(allowedMethods, allowedMethods.length);}public String getPattern() {return pattern;}/* 6. equals / hashCode / toString  与 record 默认逻辑一致 */@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof UriPattern)) return false;UriPattern that = (UriPattern) o;return Arrays.equals(allowedMethods, that.allowedMethods)&& Objects.equals(pattern, that.pattern);}@Overridepublic int hashCode() {int result = Objects.hash(pattern);result = 31 * result + Arrays.hashCode(allowedMethods);return result;}@Overridepublic String toString() {return "UriPattern[allowedMethods=" + Arrays.toString(allowedMethods)+ ", pattern=" + pattern + ']';}
}

of工厂类最为关键,作用是:

配置字符串split(“:”) 结果parts.length进入分支最终 UriPattern
get:/api/v1/user/captcha/**["get", "/api/v1/user/captcha/**"]2elseallowedMethods=["get"], pattern=/api/v1/user/captcha/**
/api/v1/user/captcha/**["/api/v1/user/captcha/**"]1ifallowedMethods=["*"], pattern=/api/v1/user/captcha/**
GET,POST:/api/health["GET,POST", "/api/health"]2elseallowedMethods=["GET","POST"], pattern=/api/health
*["*"]1ifallowedMethods=["*"], pattern=*

3、hasPerm,检查请求的uri是否是有权限的,鉴权核心

private boolean hasPerm(String uri, String method, List<String> userPerms, Map<String, List<String>> resourcePermMappings) {PathMatcher matcher = new AntPathMatcher();//1.找到访问uri所需要的权限for (Map.Entry<String, List<String>> entry : resourcePermMappings.entrySet()) {String resource = entry.getKey();//资源,模式List<String> perms = entry.getValue();UriPattern up = UriPattern.of(resource);if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {for (String perm : perms) {if (userPerms.contains(perm)) {return true;}}}}return false;}

按资源模式轮询 → 路径+方法匹配 → 任一所需权限在用户列表里即立即放行,全扫完还没命中就拒绝。

理解这三个辅助方法,思路就很明确了,我们要读取前端发过来的请求,并通过负载均衡轮询方式请求rbac服务的资源映射关系,之后就能够利用hasPerm方法来判断了,有错的就返回对应的错误信息即可,难点是解析uri

别忘了jwt验证要先做,之后再鉴权

全部代码如下:

@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest req = exchange.getRequest();String uri = req.getPath().toString();//当前请求路径String ctx = req.getPath().contextPath().value();//上下文路径uri = uri.replace(ctx, "");//无上下文的请求路径String method = req.getMethod().toString();final String finalUri = uri;//对白名单中的地址直接放行List<String> ignoreUrls = gatewayConfig.getWhiteList();PathMatcher matcher = new AntPathMatcher();for (String pattern : ignoreUrls) {UriPattern up = UriPattern.of(pattern);if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {return chain.filter(exchange);//直接放行}}String jwt = req.getHeaders().getFirst("Authorization");if (!StringUtils.hasText(jwt)) {jwt = req.getQueryParams().getFirst("jwt");}if (StringUtils.hasText(jwt)) {//校验tokenJWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();//从jwt中取出有效数据,进行业务使用,如从Redis中获取用户数据try {DecodedJWT dj = jwtVerifier.verify(jwt);//从session或Redis中取出用户数据Integer userId = dj.getClaim("userId").asInt();//用户idString username = dj.getAudience().get(0);//用户名//鉴权System.out.println(userId + ":" + username);//1.找出当前请求所需要的权限,resource_perm_mappingsWebClient webClient = webClientBuilder.build();return webClient.get().uri("http://" + gateway + "/api/v1/rbac/resource_perm_mappings").retrieve().bodyToMono(ResponseResult.class).flatMap(it -> {Map<String, List<String>> resourcePermMappings = (Map<String, List<String>>) it.getData();//2.找出当前用户所有的权限return webClient.get().uri("http://" + gateway + "/api/v1/rbac/perms/{userId}/true", userId).retrieve().bodyToMono(ResponseResult.class).flatMap(it1 -> {List<String> perms = (List<String>) it1.getData();//当前用户拥有的所有权限if (hasPerm(finalUri, method, perms, resourcePermMappings)) {//return chain.filter(exchange);//放行String authInfo = userId + ":" + username;authInfo = Base64.getEncoder().encodeToString(authInfo.getBytes());ServerHttpRequest mutatedRequest = exchange.getRequest().mutate().header("x-auth-info", authInfo).build();return chain.filter(exchange.mutate().request(mutatedRequest).build());} else {//鉴权未通过return writeJson(exchange.getResponse(), HttpStatus.FORBIDDEN, ResponseResult.errorResult(-1,"无权访问"));}});});} catch (JWTVerificationException e) {log.log(Level.SEVERE, "jwt校验异常", e);return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED,ResponseResult.errorResult(-1,"jwt无效或已过期"));}} else {return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED, ResponseResult.errorResult(-1,"无jwt,请重新认证"));}}

问题来了:A服务远程调用B服务时,该怎么确定他有没有权限呢?

答:通过AOP切面编程,先鉴权再进入方法执行。

具体做法:

1、获取用户信息放到当前线程中:(Servlet 容器(Tomcat)默认是“一个请求 = 一条线程”全程处理)

上面的鉴权系统,只能进行鉴权,无法将已经鉴权的用户信息进行保存,因此这里要新建一个拦截器来进行操作

public class AuthHandlerInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) throws Exception {//从请求头中获取认证信息,经过base64编码过的String base64AuthInfo = req.getHeader(AuthInfo.AUTH_INFO_HEADER_KEY);if (base64AuthInfo == null) {base64AuthInfo = req.getParameter(AuthInfo.AUTH_INFO_HEADER_KEY);//尝试从请求参数中获取(针对特殊情况)}if (StringUtils.hasText(base64AuthInfo)) {//base64解码String authInfo = new String(getDecoder().decode(base64AuthInfo));String[] arr = authInfo.split(":");String userId = arr[0];//用户id,非主键String userName = arr[1];//用户名//设置到当前线程中AuthInfo.setCurrent(AuthInfo.of(userId, userName));}return true;}
}

这里的AuthInfo是自定义类:

/*** 认证信息记录类,用于封装用户认证相关信息* 使用Java record类型实现,自动生成构造方法、getter、equals、hashCode和toString方法*/
public record AuthInfo(String userId, String username) {/*** 认证信息请求头名称常量*/public static final String AUTH_INFO_HEADER_KEY = "x-auth-info";/*** 空认证信息实例,表示匿名用户*/public static final AuthInfo EMPTY = AuthInfo.of("0", "匿名用户");/*** 线程本地变量,用于存储当前线程的认证信息*/private static final ThreadLocal<AuthInfo> INFO_THREAD_LOCAL = new ThreadLocal<>();/*** 创建认证信息实例的工厂方法* @param userId 用户ID* @param userName 用户名* @return 新的AuthInfo实例*/public static AuthInfo of(String userId, String userName) {return new AuthInfo(userId, userName);}/*** 获取当前线程的认证信息* @return 当前线程的认证信息,如果不存在则返回空认证信息*/public static AuthInfo current() {AuthInfo ai = INFO_THREAD_LOCAL.get();return ai != null ? ai : EMPTY;}/*** 设置当前线程的认证信息* @param info 要设置的认证信息,不能为null* @throws NullPointerException 如果传入的认证信息为null*/public static void setCurrent(AuthInfo info) {Objects.requireNonNull(info, "用户认证信息不可为空");INFO_THREAD_LOCAL.set(info);}public static void clear() {INFO_THREAD_LOCAL.remove();}
}

2、创建方法拦截器,并加上增加/修改时间等信息

/*** aop,拦截所有业务操作,如果是保存或修改操作,如果参数是继承自AuditEntity的,则自动填充创建时间和更新时间*/
public class AutoAuditMethodInterceptor implements MethodInterceptor {@Overridepublic Object invoke(@NonNull MethodInvocation invocation) throws Throwable {String method = invocation.getMethod().getName();if (method.equals("save") || method.equals("update")) {Object[] args = invocation.getArguments();if (args.length > 0) {Object arg0 = args[0];//首参if (arg0 instanceof BaseModel ae) {AuthInfo authInfo = AuthInfo.current();ae.setUpdatedBy(authInfo.username());ae.setUpdatedTime(LocalDateTime.now());if (method.equals("save")) {ae.setCreatedBy(authInfo.username());ae.setCreatedTime(LocalDateTime.now());}}}}return invocation.proceed();}
}

3、创建全局配置类

/*** 用于微服务自动获取当前认证用户信息的自动配置。* 当前配置类和Advisor上添加@Role(BeanDefinition.ROLE_INFRASTRUCTURE),是为了避免进行代理检查,导致控制台出现警告(对程序正常运行无影响)*/
@AutoConfiguration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalAutoConfiguration implements WebMvcConfigurer {private ApplicationContext applicationContext;@Autowiredpublic void setApplicationContext(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {try {AuthHandlerInterceptor interceptor = applicationContext.getBean(AuthHandlerInterceptor.class);registry.addInterceptor(interceptor);} catch (BeansException e) {//do nothing...}}/*** 创建拦截器,获取当前认证用户信息** @return 拦截器实例*/@ConditionalOnProperty(prefix = "shoplook2025.services.auto-get-auth", name = "enabled", havingValue = "true", matchIfMissing = true)@ConditionalOnMissingBean(name = "authHandlerInterceptor")@Beanpublic AuthHandlerInterceptor authHandlerInterceptor() {return new AuthHandlerInterceptor();}/*** 创建切面,自动为审计类型的模型类,添加创建时间和更新时间,以及创建人、更新人* 注意:必须必须添加@EnableAspectJAutoProxy注解,否则Advisor不生效** @return 切面实例*/@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@Beanpublic Advisor autoAuditAspect() {AspectJExpressionPointcut pc = new AspectJExpressionPointcut();//execution表达式默认仅匹配指定类中直接声明的方法。若方法定义在父类或接口中,但未被实现类重写,则不会被识别pc.setExpression("execution(* com.situ.shoplook2025.*.api.service.impl.*.*(..))");return new DefaultPointcutAdvisor(pc, new AutoAuditMethodInterceptor());}
}

文章转载自:

http://iIR90VsD.rrtdn.cn
http://fkDCr3V3.rrtdn.cn
http://nFAApx94.rrtdn.cn
http://JUYw67ks.rrtdn.cn
http://VEAWay7q.rrtdn.cn
http://1JO2LIQn.rrtdn.cn
http://FDtLGjxe.rrtdn.cn
http://2Q3Q38t5.rrtdn.cn
http://Syvmp9gp.rrtdn.cn
http://ptED9P6g.rrtdn.cn
http://oCHckeVx.rrtdn.cn
http://YXhVnItN.rrtdn.cn
http://NnavvpfL.rrtdn.cn
http://OttAWX0g.rrtdn.cn
http://PygN9Hdt.rrtdn.cn
http://HhiywiEz.rrtdn.cn
http://CN5KqcfH.rrtdn.cn
http://pmq3Aa2q.rrtdn.cn
http://mHTBnDno.rrtdn.cn
http://B7YTLOGl.rrtdn.cn
http://ZOl5Uw9M.rrtdn.cn
http://MWM5j9ZH.rrtdn.cn
http://C3gGvATg.rrtdn.cn
http://P5pbiwlE.rrtdn.cn
http://0oHYWmxh.rrtdn.cn
http://X1gCieju.rrtdn.cn
http://ZqnNohlG.rrtdn.cn
http://9HeJFhyq.rrtdn.cn
http://8sz3c1CV.rrtdn.cn
http://IzhGJaCD.rrtdn.cn
http://www.dtcms.com/a/383157.html

相关文章:

  • Debian13下使用 Vim + Vimspector + ST-LINK v2.1 调试 STM32F103 指南
  • 临床研究三千问——临床研究体系的4个核心(9)
  • 高光谱成像在回收塑料、纺织、建筑废料的应用
  • LeetCode 2348.全0子数组的数目
  • OCSP CDN HTTPS OTA
  • 1.2.3、从“本事务读”和“阻塞别的事务”角度看 Mysql 的事务和锁
  • MySQL C API 的 mysql_init 函数深度解析
  • 第10课:实时通信与事件处理
  • 33.网络基础概念(三)
  • Spark专题-第一部分:Spark 核心概述(1)-Spark 是什么?
  • 使用buildroot创建自己的linux镜像
  • MapReduce核心知识点总结:分布式计算的基石
  • 当大模型走向“赛场”:一场跨越教育、医疗与星辰的AI创新马拉松
  • 2025年IEEE TCE SCI2区,不确定环境下多无人机协同任务的时空优化动态路径规划,深度解析+性能实测
  • Python 上下文管理器:优雅解决资源管理难题
  • 主流反爬虫、反作弊防护与风控对抗手段
  • C语言柔性数组详解与应用
  • 【C++】22. 封装哈希表实现unordered_set和unordered_map
  • ARM Cortex-M 中的 I-CODE 总线、D-CODE 总线和系统总线
  • HTML5和CSS3新增的一些属性
  • 用C语言打印乘法口诀表
  • Docker desktop安装Redis Cluster集群
  • 拼多多返利app的服务自动扩缩容策略:基于K8s HPA的弹性架构设计
  • 每日前端宝藏库 | Lodash
  • LeetCode 978.最长湍流子数组
  • Java连接电科金仓数据库(KingbaseES)实战指南
  • 2025 年 AI 与网络安全最新趋势深度报告
  • PDF发票提取工具快速导出Excel表格
  • 2. BEV到高精地图的全流程,本质上是自动驾驶**车端(车载系统上传bev到云端)与云端(云端平台处理这些bev形成高精地图)协同工作
  • Nature 子刊:儿童情绪理解的认知发展机制