TTL缓存用户数据
ThreadLocal 使用场景
用户会话信息
public class UserContext {private static ThreadLocal<String> userHolder = ThreadLocal.withInitial(() -> null);public static void setUser(String user) {userHolder.set(user);}public static String getUser() {return userHolder.get();}public static void clear() {userHolder.remove();}
}// 在某个请求处理线程中使用
UserContext.setUser("UserA");
String currentUser = UserContext.getUser();
System.out.println("Current User: " + currentUser);
UserContext.clear();
格式化工具
public class DateFormatter {private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));public static String format(Date date) {return dateFormatHolder.get().format(date);}
}// 在某个线程中使用
String formattedDate = DateFormatter.format(new Date());
System.out.println("Formatted Date: " + formattedDate);
上下文日志信息传递
public class LogContext {private static ThreadLocal<String> requestIdHolder = ThreadLocal.withInitial(() -> null);public static void setRequestId(String requestId) {requestIdHolder.set(requestId);}public static String getRequestId() {return requestIdHolder.get();}public static void clear() {requestIdHolder.remove();}
}// 在某个请求处理线程中使用
LogContext.setRequestId("123456");
String requestId = LogContext.getRequestId();
System.out.println("Request ID: " + requestId);
LogContext.clear();
TransmittableThreadLocal (TTL)
- TTL 开源地址:https://github.com/alibaba/transmittable-thread-local
TTL实现原理
- 上下文拷贝:在任务提交时,TTL 会拷贝当前线程的上下文到任务中。
- 任务执行前设置上下文:在任务执行前,TTL 会将拷贝的上下文设置到当前线程中。
- 任务执行后清理上下文:任务执行完毕后,TTL 会清理线程中的上下文,防止内存泄漏。
TTL使用场景
- 分布式追踪:在分布式系统中传递追踪 ID,方便日志的关联和问题排查。
- 事务管理:在分布式事务中传递事务上下文,确保事务的一致性。
- 上下文信息传递:在多线程环境中传递用户会话、请求上下文等信息。
#### TTL与ThreadLocal对比
Feature | ThreadLocal | TTL |
---|---|---|
上下文传递 | 仅在当前线程内存储, 无法跨线程传递 | 能够在线程池和多线程框架中传递上下文信息 |
线程复用支持 | 线程池复用线程时无法保证变量一致性 | 支持线程池复用, 确保变量在任务间传递和保持一致 |
无侵入性 | 手动管理变量设置和清除 | 替换ThreadLocal即可自动管理上下文传递和清除 |
集成方便 | 适用于简单线程环境 | 可与各种线程池和多线程框架无缝集成 |
使用场景 | 适用于单线程或不用跨线程传递上下文 | 适用于复杂多线程环境, 特别是跨线程传递上下文 |
TTL实战
- 将用户登录后的信息保存在上下文变量中,并进行跨线程之间传递
- 用户登录之后会返回 token,之后的请求将会带上这个 token,当然 token 中会携带有用户的信息,
- 所有请求最先经过网关的过滤器 AuthFilter,在过滤器中用户信息放到请求头,
- 所有请求经过网关后会来到自定义请求头拦截器 HeaderInterceptor,
- 在拦截器中拿出请求头中的用户信息放到 TTL 中,链路上的服务可以直接从 TTL 中取出用户信息
/*** 网关鉴权** @author canghe*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间// 排除过滤的 uri 地址,nacos自行添加@Autowiredprivate IgnoreWhiteProperties ignoreWhite;@Autowiredprivate RedisService redisService;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpRequest.Builder mutate = request.mutate();String url = request.getURI().getPath();// 跳过不需要验证的路径if (StringUtils.matches(url, ignoreWhite.getWhites())) {return chain.filter(exchange);}String token = getToken(request);if (StringUtils.isEmpty(token)) {return unauthorizedResponse(exchange, "令牌不能为空");}Claims claims = JwtUtils.parseToken(token);if (claims == null) {return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");}String userkey = JwtUtils.getUserKey(claims);boolean islogin = redisService.hasKey(getTokenKey(userkey));if (!islogin) {return unauthorizedResponse(exchange, "登录状态已过期");}String userid = JwtUtils.getUserId(claims);String username = JwtUtils.getUserName(claims);if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {return unauthorizedResponse(exchange, "令牌验证失败");}// 设置用户信息到请求addHeader(mutate, SecurityConstants.USER_KEY, userkey);addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);// 内部请求来源参数清除(防止网关携带内部请求标识,造成系统安全风险)removeHeader(mutate, SecurityConstants.FROM_SOURCE);//先记录下访问接口的开始时间exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());return chain.filter(exchange.mutate().request(mutate.build()).build());}private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {if (value == null) {return;}String valueStr = value.toString();
}
- 在过滤器
**AuthFilter**
中将用户信息放到请求头
/*** 自定义请求头拦截器,将Header数据封装到线程变量中方便获取* 注意:此拦截器会同时验证当前用户有效期自动刷新有效期** @author canghe*/
public class HeaderInterceptor implements AsyncHandlerInterceptor {// 需要免登录的路径集合private static final Set<String> EXEMPTED_PATHS = new HashSet<>();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");}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}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));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;}// 判断请求路径是否匹配特定路径private boolean isExemptedPath(String requestURI) {// 你可以根据需要调整特定路径的匹配逻辑return EXEMPTED_PATHS.stream().anyMatch(requestURI::startsWith);}// 创建一个默认的 LoginUser 对象private LoginUser createDefaultLoginUser() {LoginUser defaultLoginUser = new LoginUser();defaultLoginUser.setUserId(173L); // 设置默认的用户IDdefaultLoginUser.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;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {SecurityContextHolder.remove();}
}