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

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

  • 用户登录后签署的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://www.pswp.cn/news/922417.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922417.shtml
英文地址,请注明出处:http://en.pswp.cn/news/922417.shtml

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

相关文章

【算法】day1 双指针

1、移动零&#xff08;同向分3区域&#xff09; 283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 思路&#xff1a;注意原地操作。快排也是这个方法&#xff1a;左边小于等于 tmp&#xff0c;右边大于 tmp&#xff0c;最后 tmp 放到 dest。 代码&#…

Linux 日志分析:用 ELK 搭建个人运维监控平台

Linux 日志分析&#xff1a;用 ELK 搭建个人运维监控平台 &#x1f31f; Hello&#xff0c;我是摘星&#xff01; &#x1f308; 在彩虹般绚烂的技术栈中&#xff0c;我是那个永不停歇的色彩收集者。 &#x1f98b; 每一个优化都是我培育的花朵&#xff0c;每一个特性都是我放飞…

Linux网络:socket编程UDP

文章目录前言一&#xff0c;socket二&#xff0c;服务端socket3-1 创建socket3-2 绑定地址和端口3-3 接收数据3-4 回复数据3-5关闭socket3-6 完整代码三&#xff0c;客户端socket3-1 为什么客户端通常不需要手动定义 IP 和端口前言 学习 socket 编程的意义在于&#xff1a;它让…

【从零到公网】本地电脑部署服务并实现公网访问(IPv4/IPv6/DDNS 全攻略)

从零到公网&#xff1a;本地电脑部署服务并实现公网访问&#xff08;IPv4/IPv6/DDNS 全攻略&#xff09; 适用场景&#xff1a;本地 API 服务、大模型推理服务、NAS、远程桌面等需要公网访问的场景 关键词&#xff1a;公网 IP、端口映射、内网穿透、IPv6、Cloudflare DDNS 一、…

模块二 落地微服务

11 | 服务发布和引用的实践 服务发布和引用常见的三种方式&#xff1a;Restful API、XML配置以及IDL文件。今天我将以XML配置方式为例&#xff0c;给你讲解服务发布和引用的具体实践以及可能会遇到的问题。 XML配置方式的服务发布和引用流程 1. 服务提供者定义接口 服务提供者发…

C++程序员速通C#:从Hello World到数据类型

C程序员光速入门C#&#xff08;一&#xff09;&#xff1a;总览、数据类型、运算符 一.Hello world&#xff01; 随着.NET的深入人心,作为一个程序员&#xff0c;当然不能在新技术面前停而止步&#xff0c;面对着c在.net中的失败,虽然有一丝遗憾&#xff0c;但是我们应该认识到…

Linux相关概念和易错知识点(44)(IP地址、子网和公网、NAPT、代理)

目录1.IP地址&#xff08;1&#xff09;局域网和公网①局域网a.网关地址b.局域网通信②运营商子网③公网&#xff08;2&#xff09;NAPT①NAPT过程②理解NAPT③理解源IP和目的IPa.目的IPb.源IP③最长前缀匹配④NAT技术缺陷2.代理服务&#xff08;1&#xff09;正向代理&#xf…

工业智能终端赋能自动化生产线建设数字化管理

在当今数字化浪潮的推动下&#xff0c;自动化生产线正逐渐成为各行各业提升效率和降低成本的重要选择。随着智能制造的深入发展&#xff0c;工业智能终端的引入不仅为生产线带来了技术革新&#xff0c;也赋予了数字化管理新的动力。一、工业智能终端&#xff1a;一体化设计&…

【Vue2手录06】计算属性Computed

一、表单元素的v-model绑定&#xff08;核心场景&#xff09; v-model 是Vue实现“表单元素与数据双向同步”的语法糖&#xff0c;不同表单元素的绑定规则存在差异&#xff0c;需根据元素类型选择正确的绑定方式。 1.1 四大表单元素的绑定规则对比表单元素类型绑定数据类型核心…

FPGA入门-数码管静态显示

19. 数码管的静态显示 在许多项目设计中&#xff0c;我们通常需要一些显示设备来显示我们需要的信息&#xff0c;可以选择的显示设备有很多&#xff0c;而数码管是使用最多&#xff0c;最简单的显示设备之一。数码管是一种半导体发光器件&#xff0c;具有响应时间短、体积小、…

深入理解大语言模型(5)-关于token

到目前为止对 LLM 的描述中&#xff0c;我们将其描述为一次预测一个单词&#xff0c;但实际上还有一个更重要的技术细 节。即 LLM 实际上并不是重复预测下一个单词&#xff0c;而是重复预测下一个 token 。对于一个句子&#xff0c;语言模型会 先使用分词器将其拆分为一个个 to…

视觉智能的「破壁者」——Transformer如何重塑计算机视觉范式?三大CV算法论文介绍 ViTMAESwin Transformer

当自然语言处理领域因Transformer而焕发新生时&#xff0c;计算机视觉却长期困于卷积神经网络的架构桎梏。直到ViT&#xff08;Vision Transformer&#xff09;的横空出世&#xff0c;才真正打破了视觉与语言之间的壁垒。它不仅是技术的革新&#xff0c;更是范式革命的开始&…

Java 并发容器源码解析:ConcurrentSkipListSet 行级深度剖析

Java 并发容器源码解析&#xff1a;ConcurrentSkipListSet 行级深度剖析 本文将深入解析 Java 并发容器 ConcurrentSkipListSet 的核心源码&#xff0c;结合流程图、代码注释、设计思想、优缺点分析、业务场景、调试与优化、集成方案、高阶应用等&#xff0c;帮助你系统掌握这款…

答题卡自动识别案例

目录 1.答题卡自动批阅整体实现思路 2.关键技术步骤与原理 答题卡区域提取 ①轮廓检测并排序 ②执行透视变换 ③找到每一个圆圈轮廓 ④先对所有圆圈轮廓从上到下排序 ⑤再通过循环每次只提取出五个轮廓再进行从左到右的排序 3.完整代码 1.答题卡自动批阅整体实现思路 …

C#实现通过POST实现读取数据

C# POST请求与MySQL数据存储实现下面是一个完整的C#解决方案&#xff0c;用于发送POST请求、接收响应数据&#xff0c;并将数据保存到MySQL数据库中。完整代码实现 using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.J…

Java 字符编码问题,怎么优雅地解决?

网罗开发&#xff08;小红书、快手、视频号同名&#xff09;大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等方…

STL之string类(C++)

1.string类核心定位std::string 本质是对 “字符序列” 的封装&#xff0c;内部通过动态数组存储字符&#xff0c;并自动管理内存&#xff08;分配、扩容、释放&#xff09;&#xff0c;对外提供了简洁的接口用于字符串的创建、修改、拼接、查找等操作。1.1 使用前提头文件包含…

[Maven 基础课程]第一个 Maven 项目

idea 新建一个项目&#xff1a; 来到 New Project 页面&#xff1a; 这里我们有两种方式创建 maven 项目&#xff0c;一种是自定义创建&#xff0c;另一种是使用 maven 模版项目创建。 自定义创建 maven 项目 基本配置 Name: first_maven_project 项目名称&#xff0c;设为 …

uni小程序中使用Echarts图表

前言 今天鸡米花给大家带来的是在uni里面使用echarts&#xff0c;能够完美支持和PC端一样的效果&#xff0c;我这边的工程是uni转为微信小程序&#xff0c;用的是vue3vite来写的&#xff0c;然后实现了竖屏和横屏的展示方式&#xff0c;好了献上效果图。 效果图 一、引入插件 这…

从FOTA测试到汽车电子安全体系的启蒙之旅

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…