// 拦截器校验所有非登录请求时的token,校验成功之后解析出用户信息存入ThreadLocal中便于本次请求中共享该用户的信息,这个信息只能在本线程中拿到
一、需求分析
在用户登录后的请求交互中,Token 的校验是保障用户身份合法性和数据安全的重要环节。通过校验 Token,可以有效防止非法请求、数据篡改等安全问题,同时实现基于用户身份的权限管理。
功能需求
-
Token 校验逻辑:
- 后端在接收到用户发起的非登录请求时,从请求头中提取 Token。
- 对 Token 进行解密和验证,确保其合法性(例如签名校验和时间有效性)。
- 如果 Token 不合法(为空或解密失败),则返回 401 错误,提示用户重新登录。
- 如果 Token 合法,从中解析出用户的身份信息(如用户 ID 和用户名)。
-
用户身份获取:
- 将解析出的用户身份信息存储到当前线程的
ThreadLocal
中,便于后续业务层和控制层直接调用,避免多次重复解析。
- 将解析出的用户身份信息存储到当前线程的
-
业务请求支持:
- 在业务层中,能够通过校验后的用户身份信息,执行相应的操作(如查询用户专属数据、验证用户权限等)。
流程需求
-
登录阶段:
- 用户登录成功后,后端生成 JWT Token 并返回给前端。
- 前端将 Token 存储在本地(如
localStorage
或sessionStorage
),并在后续请求中携带该 Token。
-
校验阶段:
- 在后端,通过拦截器(Interceptor)拦截非登录请求,提取请求头中的 Token。
- 对 Token 进行解析和校验,确保用户身份的合法性。
- 校验成功后,将用户身份信息存储到
ThreadLocal
,供业务逻辑调用。
-
响应阶段:
- 如果校验失败,返回错误信息(如 401 未授权)。
- 如果校验成功,继续执行对应的业务逻辑。
二、代码解读
拦截器类:UserInterceptor
功能:实现 HandlerInterceptor
接口,对所有请求进行拦截,校验 Token,解析用户身份并存储到 ThreadLocal
中。
方法 preHandle
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法就直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 获取请求头中的 Token
String token = request.getHeader(Constants.USER_TOKEN);
log.info("开始解析 customer user token:{}", token);
// Token 为空或无效时,抛出异常
if (ObjectUtil.isEmpty(token)) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
// 解析 Token 并获取载荷信息
Map<String, Object> claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), token);
if (ObjectUtil.isEmpty(claims)) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
// 获取用户 ID
Long userId = MapUtil.get(claims, Constants.JWT_USERID, Long.class);
if (ObjectUtil.isEmpty(userId)) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
// 将用户 ID 存入当前线程中
UserThreadLocal.set(userId);
// 返回 true 允许请求继续执行
return true;
}
-
逻辑说明:
- 判断当前请求是否映射到方法(
HandlerMethod
)。如果不是(例如静态资源或跨域请求),直接放行。 - 从请求头中获取 Token(
authorization
),并检查其是否为空。 - 通过
JwtUtil.parseJWT
方法解析 Token,校验其合法性。 - 从解析后的载荷中提取用户 ID,并存入
ThreadLocal
,供后续业务逻辑调用。 - 如果校验通过,返回
true
,请求继续执行;否则抛出异常。
- 判断当前请求是否映射到方法(
-
安全保障:
- Token 为空、无效或解析失败时,都会抛出异常,阻止非法请求进入业务逻辑层。
- 使用
ThreadLocal
确保当前线程独立存储用户身份信息,避免多线程访问数据冲突。
-
异常处理:
- 抛出自定义异常
BaseException
,状态码和错误信息由BasicEnum
枚举定义。 - 典型错误场景包括:Token 缺失、Token 解析失败、用户 ID 为空等。
- 抛出自定义异常
方法 afterCompletion
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 响应结束时清理 ThreadLocal 中的数据,防止内存泄漏
UserThreadLocal.remove();
}
-
逻辑说明:
- 每次请求完成后,都会清理
ThreadLocal
中存储的用户信息,避免数据在线程间污染。 - 此方法是 Spring 的拦截器生命周期方法之一,在请求完成后被自动调用。
- 每次请求完成后,都会清理
-
目的:
- 防止内存泄漏和线程间数据污染,确保系统运行的稳定性和安全性。
常量类:Constants
public static final String USER_TOKEN = "authorization";
- 定义了请求头中携带 Token 的字段名
authorization
。 - 所有请求都会从请求头中提取该字段的值进行校验。
Web MVC 配置类:WebMvcConfig
- 功能:通过 Spring 的拦截器注册机制,将
UserInterceptor
配置为全局拦截器,并对某些接口进行过滤。
拦截器注册
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(userInterceptor)
// 排除不需要拦截的路径(如 Swagger 文档、登录接口等)
.excludePathPatterns(EXCLUDE_PATH_PATTERNS)
// 指定拦截的路径
.addPathPatterns("/customer/**");
}
-
addInterceptor
:- 将自定义拦截器
userInterceptor
注册到拦截器链中,作用于指定的请求路径。
- 将自定义拦截器
-
excludePathPatterns
:- 定义了拦截器的排除路径,以下场景不会触发拦截:
- Swagger 相关路径:
/swagger-ui.html
、/v2/api-docs
等。 - 登录接口:
/customer/user/login
,无需校验 Token。
- Swagger 相关路径:
- 定义了拦截器的排除路径,以下场景不会触发拦截:
-
addPathPatterns
:- 定义需要拦截的路径,这里拦截所有以
/customer/**
开头的接口请求。
- 定义需要拦截的路径,这里拦截所有以
UserThreadLocal
类
/**
* subjectContent.java
* 用户主体对象
*/
@Slf4j
public class UserThreadLocal {
/***
* 创建线程局部userVO变量
*/
public static ThreadLocal<String> subjectThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return null;
}
};
// 提供线程局部变量set方法
public static void setSubject(String subject) {
subjectThreadLocal.set(subject);
}
// 提供线程局部变量get方法
public static String getSubject() {
return subjectThreadLocal.get();
}
//清空当前线程,防止内存溢出
public static void removeSubject() {
subjectThreadLocal.remove();
}
private static final ThreadLocal<Long> LOCAL = new ThreadLocal<>();
private UserThreadLocal() {
}
/**
* 将authUserInfo放到ThreadLocal中
*
* @param authUserInfo {@link Long}
*/
public static void set(Long authUserInfo) {
LOCAL.set(authUserInfo);
}
/**
* 从ThreadLocal中获取authUserInfo
*/
public static Long get() {
return LOCAL.get();
}
/**
* 从当前线程中删除authUserInfo
*/
public static void remove() {
LOCAL.remove();
}
/**
* 从当前线程中获取前端用户id
* @return 用户id
*/
public static Long getUserId() {
return LOCAL.get();
}
/**
* 从当前线程中获取前端后端id
* @return 用户id
*/
public static Long getMgtUserId() {
String subject = subjectThreadLocal.get();
if (ObjectUtil.isEmpty(subject)) {
return null;
}
BaseVo baseVo = JSONObject.parseObject(subject, BaseVo.class);
return baseVo.getId() ;
}
}
1. setSubject(String subject)
- 功能:将用户主体信息(字符串)存储到当前线程的
subjectThreadLocal
中。 - 使用场景:解析用户主体信息后调用,便于后续获取。
2. getSubject()
- 功能:从当前线程的
subjectThreadLocal
中获取用户主体信息。 - 使用场景:需要访问当前线程用户主体信息时调用。
3.removeSubject()
- 功能:清理当前线程的
subjectThreadLocal
,防止内存泄漏。 - 使用场景:请求结束时调用,确保线程数据不被污染。
三、总结
-
校验 Token:
- 后端通过
UserInterceptor
拦截请求,从请求头中提取 Token。 - 使用工具类
JwtUtil
解析 Token,验证其合法性、有效性。 - 将解析后的用户身份信息存入
ThreadLocal
,供业务层使用。
- 后端通过
-
ThreadLocal 的应用:
- 使用
UserThreadLocal
工具类隔离多线程环境下的用户数据,避免数据冲突。 - 在请求完成后,清理
ThreadLocal
中的用户信息,防止内存泄漏。
- 使用
-
拦截器的灵活性:
- 配置了拦截路径与排除路径,确保仅对需要校验的接口进行拦截(例如,登录接口不需要校验 Token)。