在分布式应用场景下,我们可以利用网关对请求进行集中处理,实现了低耦合,高内聚的特性。
登陆权限验证和鉴权的功能都可以在网关层面进行处理:
- 用户登录后签署的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 的 body | body 长度为 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/**"] | 2 | else | allowedMethods=["get"], pattern=/api/v1/user/captcha/** |
/api/v1/user/captcha/** | ["/api/v1/user/captcha/**"] | 1 | if | allowedMethods=["*"], pattern=/api/v1/user/captcha/** |
GET,POST:/api/health | ["GET,POST", "/api/health"] | 2 | else | allowedMethods=["GET","POST"], pattern=/api/health |
* | ["*"] | 1 | if | allowedMethods=["*"], 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());}
}