大家好,我是苏三。
今天来解决一个粉丝的问题:在SpringCloud Gateway实现鉴权时,如何在转发请求到子业务模块时获取登录用户信息?
首先从请求头中获取 Authorization 参数得到 JWT Token,然后解开 JWT 后即可获取用户信息。
以下是相关代码:
@Override
public AuthenticatorResult auth(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders httpHeaders = request.getHeaders();
// 获取JWT请求头 Authorization
String token = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (Objects.nonNull(token)) {
try {
String subjectFromJWT = JwtUtil.getSubjectFromJWT(token);
log.info("用户请求token: {} , 身份Subject:{}", token, subjectFromJWT);
returnnew AuthenticatorResult(true, "认证通过");
} catch (ParseException | JOSEException e) {
log.error("token解析失败{}",token);
returnnew AuthenticatorResult(false, "Token错误,请重新登录!");
}
}
returnnew AuthenticatorResult(false, "Token为空,请重新登录!");
}
实现方案
通常情况下,在 Spring Cloud 中将用户信息透传给后端服务有两种方式:
第一种:在网关解析出用户的 Token 得到用户 ID,然后将用户 ID 添加到请求头中传递下去。
第二种:在网关直接把 Token 传递下去,由各个子服务自行解析。
在 DailyMart 中我推荐使用第一种方式,将 JWT Token 解析后直接透传给后端服务。
以下是实现方案:
- 在网关解析 JWT Token 后得到 UserID,修改 Spring Cloud Gateway 的请求头,将 UserID 添加到请求头中。
- 自定义用户上下文
UserContextHolder,并使用 ThreadLocal 进行存储。 - 在微服务中的 Web 组件中创建拦截器
UserTokenInterceptor,从 Request 中获取 UserID,并将其添加到用户上下文UserContextHolder中。 - 将拦截器
UserInterceptor注册到 Spring 容器中。
代码实现
1、在SpringCloud Gateway中修改请求头
public class DefaultApiAuthenticator implements ApiAuthenticator {
@Override
public AuthenticatorResult auth(ServerWebExchange exchange) {
...
if (Objects.nonNull(token)) {
try {
String subjectFromJWT = JwtUtil.getSubjectFromJWT(token);
//重新设置请求头
mutateNewHeader(exchange, subjectFromJWT);
returnnew AuthenticatorResult(true, "认证通过");
} catch (ParseException | JOSEException e) {
log.error("token解析失败");
returnnew AuthenticatorResult(false, "Token错误,请重新登录!");
}
}
returnnew AuthenticatorResult(false, "Token为空,请重新登录!");
}
/**
* 重新构建请求头,将用户账号放入Token
* @param subject 用户身份
*/
private void mutateNewHeader(ServerWebExchange exchange, String subject) {
ServerHttpRequest newRequest = exchange.getRequest().mutate().header(CommonConstant.X\_CLIENT\_TOKEN, subject).build();
exchange.mutate().request(newRequest).build();
}
}
2、自定义用户上下文UserContextHolder
public class UserContextHolder {
privatefinal ThreadLocal<String> threadLocal;
private UserContextHolder() {
this.threadLocal = new ThreadLocal<>();
}
public static UserContextHolder getInstance() {
return SingletonHolder.instance;
}
/**
* 设置用户数据
* @param userId 用户账号
*/
public void setCurrentUser(String userId) {
this.threadLocal.set(userId);
}
/**
* 获取当前用户
* @return userId
*/
public String getCurrentUser() {
returnthis.threadLocal.get();
}
/**
* 清理用户信息
*/
public void clear() {
this.threadLocal.remove();
}
}
3、创建自定义拦截器UserTokenInterceptor
由于每个服务中都需要用到此功能,所以我们将此功能在公共组件dailymart-web-spring-boot-starter中实现。
public class UserTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
String userId = request.getHeader(CommonConstant.X\_CLIENT\_TOKEN);
// 设置用户信息
UserContextHolder.getInstance().setCurrentUser(userId);
returntrue;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
UserContextHolder.getInstance().clear();
}
}
这里先从请求头中获取userId,然后使用单例类UserContextHolder将userId添加到ThreadLocal中,当然在处理完成后需要清空ThreadLocal的值,不然会出现内存泄露。
4、创建配置类,在Spring中注册拦截器
@SpringBootConfiguration
@ConditionalOnWebApplication
publicclass WebMvcConfigurerAdaptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor())
.addPathPatterns("/api/pd/**")
.excludePathPatterns(excludePathList);
}
privatefinal String[] excludePathList = new String[]{
"/api/pd/customer/login"
};
@Bean
public HandlerInterceptor userInterceptor() {
returnnew UserTokenInterceptor();
}
}
在上篇文章中我们已经规定来源标识为/pd的才算是浏览器的请求,所以在这个拦截器中我们只需要配置特定的拦截地址即可。
5、获取userId
在后续微服务中如果需要获取 UserID,只需从 UserContextHolder 获取即可:
@Operation(summary = "用户测试接口")
@PostMapping("/api/pd/customer/info")
public void info() {
String currentUser = UserContextHolder.getInstance().getCurrentUser();
log.info("当前登录用户:" + currentUser);
}
小结
本文介绍了在 Spring Cloud Gateway 中实现鉴权并将用户信息传递给后端服务的解决方案。
通过解析 JWT Token 并将用户身份信息添加到请求头,实现了简单而有效的用户身份认证和信息传递。
当然,文章中仅仅传递了UserID,在实际使用中,大家也可以先在网关层通过UserID获取用户的详细信息,再将详细信息进行传递。
- End-
最后欢迎加入苏三的星球,你将获得:智能天气播报AI Agent、SaaS点餐系统(DDD+多租户)、100万QPS短链系统(超过并发)、复杂的商城微服务系统(分布式)、苏三商城系统、苏三AI项目、刷题吧小程序、秒杀系统、码猿简历网站、代码生成工具等10个项目的源代码、开发教程和技术答疑。 系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。
还有1V1免费修改简历、技术答疑、职业规划、送书活动、技术交流。
扫描下方二维码,可以加入星球:
数量有限,先到先得。 目前星球已经更新了6100+篇优质内容,还在持续爆肝中.....
