TTL
基于 TransmittableThreadLocal (TTL) 自定义请求头拦截器,将Header 数据封装到线程变量中方便获取,减少用户信息数据库查询次数,同时验证当前用户有效期自动刷新有效期。
Categories:
TransmittableThreadLocal
(TTL) 是增强版的 ThreadLocal
前置:
ThreadLocal
, 拦截器
实现TTL
流程图如下
前置: Gateway网关
上半部分业务为用户请求登录并返回Token
该业务从MVC剥离, 设置在网关进行
我们主要关注下半部分, 用户携带 Token
后如何在服务层获取用户信息
整体流程为:
-
用户登录请求:
- 用户通过提交用户名和密码进行登录,经过网关的认证,认证服务(如
Auth
)验证用户身份并生成token
。 - 登录请求返回
token
给用户,用户保存token
。
- 用户通过提交用户名和密码进行登录,经过网关的认证,认证服务(如
-
后续请求携带 Token 登录:
- 用户在后续请求中携带该
token
,这个token
用于证明用户的身份。
- 用户在后续请求中携带该
-
Nginx 负载均衡:
- 请求首先被发送到
Nginx
,Nginx 会根据负载均衡策略将请求转发到具体的 网关(Gateway
)。
- 请求首先被发送到
-
网关鉴权:
- 网关负责根据请求中的
token
进行 鉴权,验证该token
是否有效。如果验证通过,网关会继续转发请求;如果验证失败,则拒绝请求或重定向至登录页面。
- 网关负责根据请求中的
-
拦截器将用户信息放入 TTL:
- 在请求经过网关后,网关会调用拦截器(如
HeaderInterceptor
)。拦截器会提取请求中的用户信息(如token
解密后的用户信息),然后将这些信息存放到 TTL(Thread-Local)中。 - TTL 是一种线程局部存储机制,用于在当前线程内传递数据,确保在同一请求的生命周期内,后续的服务(如
ProjectService
等)能够访问到这些用户信息。
- 在请求经过网关后,网关会调用拦截器(如
-
后续服务从 TTL 获取用户信息:
- 后续的服务(如
ProjectService
)可以通过访问当前线程的 TTL 中的数据来获取用户信息,而无需每次都从请求中提取或解析token
。
- 后续的服务(如
我们所关注的 TTL 在拦截器里实现
新建 HeaderInterceptor.java
public class HeaderInterceptor implements AsyncHandlerInterceptor
该拦截器实现了拦截器接口, 该业务在请求执行之前执行拦截业务逻辑, 所以重写 preHandle
在请求执行完毕后需要删除用户信息, 所以重写 afterCompletion
public class HeaderInterceptor implements AsyncHandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { }
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
首先是 preHandle
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//不是控制器方法不拦截
if (!(handler instanceof HandlerMethod)) {
return true;
}
//自定义holder 用来设置信息
SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
//根据Token创建用户
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token)) {
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser)) {
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
} else {
// 首页免登场景展示
// 检查请求路径是否匹配特定路径
String requestURI = request.getRequestURI();
if (isExemptedPath(requestURI)) {
// 创建一个默认的 LoginUser 对象
LoginUser defaultLoginUser = createDefaultLoginUser();
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, defaultLoginUser);
}
}
return true;
}
这里使用 TTL的就是 设置信息的工具类
public class SecurityContextHolder{
//TTL
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
//设置方法, 在线程Map中存储私有信息
public static void set(String key, Object value)
{
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StringUtils.EMPTY : value);
}
//这里只就一个例子, 存储固定的用户名前缀和用户名的方法
public static void setUserName(String username)
{
set(SecurityConstants.DETAILS_USERNAME, username);
}
}
最后添加其他方法
static {
// 在这里添加所有需要免登录默认展示首页的的路径
EXEMPTED_PATHS.add("/system/user/getInfo");
EXEMPTED_PATHS.add("/project/statistics");
EXEMPTED_PATHS.add("/project/doing");
EXEMPTED_PATHS.add("/project/queryMyTaskList");
EXEMPTED_PATHS.add("/project/select");
EXEMPTED_PATHS.add("/system/menu/getRouters");
}
// 需要免登录的路径集合
private static final Set<String> EXEMPTED_PATHS = new HashSet<>();
// 判断请求路径是否匹配特定路径, 也就是免密白名单
private boolean isExemptedPath(String requestURI) {
// 你可以根据需要调整特定路径的匹配逻辑
return EXEMPTED_PATHS.stream().anyMatch(requestURI::startsWith);
}
// 创建一个默认的 LoginUser 对象
private LoginUser createDefaultLoginUser() {
LoginUser defaultLoginUser = new LoginUser();
defaultLoginUser.setUserId(173L); // 设置默认的用户ID
defaultLoginUser.setUsername(Constants.DEMO_ACCOUNT); // 设置默认的用户名
SysUser demoSysUser = new SysUser();
demoSysUser.setUserId(173L);
demoSysUser.setUserName(Constants.DEMO_ACCOUNT);
demoSysUser.setDeptId(100L);
demoSysUser.setStatus("0");
defaultLoginUser.setUser(demoSysUser);
// 设置其他必要的默认属性
return defaultLoginUser;
}
//结束后清理用户信息, 防止线程复用导致内存泄露
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
SecurityContextHolder.remove();
}