TTL

基于 TransmittableThreadLocal (TTL) 自定义请求头拦截器,将Header 数据封装到线程变量中方便获取,减少用户信息数据库查询次数,同时验证当前用户有效期自动刷新有效期。

TransmittableThreadLocal (TTL) 是增强版的 ThreadLocal

前置: ThreadLocal, 拦截器

实现TTL

流程图如下

image.png

前置: 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();  
}
Last modified March 3, 2025: 组件笔记 3/3 (41127dd)