我们已经实现了在网关层进行身份认证:首先从请求头中获取 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);
return new AuthenticatorResult(true, "认证通过");
} catch (ParseException | JOSEException e) {
log.error("token解析失败{}",token);
return new AuthenticatorResult(false, "Token错误,请重新登录!");
}
}
return new 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);
return new AuthenticatorResult(true, "认证通过");
} catch (ParseException | JOSEException e) {
log.error("token解析失败");
return new AuthenticatorResult(false, "Token错误,请重新登录!");
}
}
return new 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 {
private final 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() {
return this.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);
return true;
}
@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
public class WebMvcConfigurerAdaptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor())
.addPathPatterns("/api/pd/**")
.excludePathPatterns(excludePathList);
}
private final String[] excludePathList = new String[]{
"/api/pd/customer/login"
};
@Bean
public HandlerInterceptor userInterceptor() {
return new 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获取用户的详细信息,再将详细信息进行传递。同时,本文涉及的代码都已经上传至Github,感兴趣的可以通过文末方式获取。