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实现原理
  • 上下文拷贝:在任务提交时,TTL 会拷贝当前线程的上下文到任务中。
  • 任务执行前设置上下文:在任务执行前,TTL 会将拷贝的上下文设置到当前线程中。
  • 任务执行后清理上下文:任务执行完毕后,TTL 会清理线程中的上下文,防止内存泄漏。

TTL使用场景
  • 分布式追踪:在分布式系统中传递追踪 ID,方便日志的关联和问题排查。
  • 事务管理:在分布式事务中传递事务上下文,确保事务的一致性。
  • 上下文信息传递:在多线程环境中传递用户会话、请求上下文等信息。

#### TTL与ThreadLocal对比
FeatureThreadLocalTTL
上下文传递仅在当前线程内存储, 无法跨线程传递能够在线程池和多线程框架中传递上下文信息
线程复用支持线程池复用线程时无法保证变量一致性支持线程池复用, 确保变量在任务间传递和保持一致
无侵入性手动管理变量设置和清除替换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自行添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;


    @Override
    public 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**中将用户信息放到请求头

image.png

/**
 * 自定义请求头拦截器,将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");

    }

    @Override
    public 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);  // 设置默认的用户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();
    }
}

image.png

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值