
Sa-Token 异步与响应式上下文SaTokenContextMockUtil 与 SaReactorSyncHolder 实战指南【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token本篇技术指南聚焦 Sa-Token 权限认证框架在非 Web 上下文异步线程、定时任务、消息队列与响应式环境WebFlux / Spring Cloud中的上下文处理方案。通过阅读本文你将掌握SaTokenContextMockUtil模拟上下文、SaReactorSyncHolder同步上下文以及SaReactorHolder响应式包装器的完整使用姿势并理解其底层基于 ThreadLocal 的上下文存储原理能够在异步与响应式场景下安全调用StpUtil.isLogin()等 API。一、问题背景为什么异步上下文会抛异常Sa-Token 中有一些方法例如StpUtil.isLogin()、StpUtil.getLoginId()只可以在同步的 Web 上下文中调用。如果在异步上下文中调用会抛出如下异常cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化出现该异常的根本原因在于这些方法需要从前端的HttpServletRequest中读取 Token 参数而异步上下文通常不是一次请求不具备HttpServletRequest的概念所以无法成功调用。从源码层面看Sa-Token 的上下文信息存放在基于 ThreadLocal 的存储箱中。核心实现位于 SaTokenContextForThreadLocalStaff.java其内部通过modelBoxThreadLocalThreadLocalSaTokenContextModelBox持有当前线程的上下文对象。当子线程或非请求线程中没有被写入过上下文时getModelBox()就会抛出上述上下文尚未初始化异常。哪些场景属于异步上下文凡是不通过 Web 请求调用触发的线程在 Sa-Token 中均属于异步上下文也可以称作非 Web 上下文常见场景包括通过new Thread(() - { ... }).start()启动子线程通过taskExecutor.execute(() - { ... })线程池启动异步任务通过Async注解标注的方法通过Scheduled(cron )启动的定时任务消息队列中消费消息的函数……此时调用StpUtil.isLogin()、StpUtil.getLoginId()等需要 Web 上下文的 API就会抛出上述异常。如果你需要在非 Web 上下文中调用上述 API则需要手动 Mock 一个上下文才可以调用成功。二、异步上下文解决方案SaTokenContextMockUtilSaTokenContextMockUtil是 Sa-Token 提供的 Mock 上下文操作工具类自 1.42.0 版本起提供完整源码位于 SaTokenContextMockUtil.java。它提供了三个核心方法方法签名作用setMockContext()写入 Mock 上下文创建SaRequestForMock、SaResponseForMock、SaStorageForMock三个模拟对象并通过SaManager.getSaTokenContext().setContext(request, response, storage)填充到上下文管理器中setMockContext(SaFunction fun)写入 Mock 上下文执行一段代码执行完毕后自动清除上下文finally 中调用clearContext()T T setMockContext(SaRetGenericFunctionT fun)写入 Mock 上下文并执行有返回值的代码执行完毕后自动清除上下文并返回结果此外还提供了clearContext()方法用于手动清除上下文。典型用法new Thread 子线程// 【异步】new Thread RequestMapping(isLogin2) public SaResult isLogin2() { System.out.println(是否登录 StpUtil.isLogin()); String tokenValue StpUtil.getTokenValue(); new Thread(() - { SaTokenContextMockUtil.setMockContext(()-{ StpUtil.setTokenValueToStorage(tokenValue); System.out.println(是否登录 StpUtil.isLogin()); }); }).start(); return SaResult.data(StpUtil.getTokenValue()); }使用要点如下先调用SaTokenContextMockUtil.setMockContext(() - { ... })Mock 出一个 Web 上下文填充到上下文管理器中然后在 Mock 上下文范围内调用StpUtil.setTokenValueToStorage(tokenValue)指定当前上下文的 Token 值其效果等同于在 Web 上下文中前端提交了此 Token 值代码块执行完毕后setMockContext会在finally中自动清除上下文避免 ThreadLocal 泄漏污染其他线程。更多异步场景示例仓库中的完整可运行示例位于 sa-token-demo-async 的 TestController.java覆盖了四种典型异步场景场景一线程池ThreadPoolTaskExecutor// 【异步】线程池 ThreadPoolTaskExecutor --- http://localhost:8081/test/isLogin3 RequestMapping(isLogin3) public SaResult isLogin3(HttpServletRequest request, HttpServletResponse response) { System.out.println(是否登录 StpUtil.isLogin()); String tokenValue StpUtil.getTokenValue(); taskExecutor.execute(() - { SaTokenContextMockUtil.setMockContext(()-{ StpUtil.setTokenValueToStorage(tokenValue); System.out.println(是否登录 StpUtil.isLogin()); }); }); return SaResult.data(StpUtil.getTokenValue()); }场景二Async异步方法// 【异步】Async --- http://localhost:8081/test/isLogin4 Async RequestMapping(isLogin4) public SaResult isLogin4(CookieValue(satoken) String satoken) { SaTokenContextMockUtil.setMockContext(()-{ StpUtil.setTokenValueToStorage(satoken); System.out.println(是否登录 StpUtil.isLogin()); }); return SaResult.ok(); }场景三Scheduled定时任务// 【异步】定时任务 Scheduled(cron 0 * * * * ?) // 一分钟执行一次 // Scheduled(cron 0/10 * * * * ?) // 十秒执行一次 public void scheduledMethod(){ // 错误写法直接调用 Sa-Token API 会报错 // System.out.println(定时任务Mock 范围外是否登录 StpUtil.isLogin()); System.out.println(SaFoxUtil.formatDate(new Date())); // 需要先设置模拟上下文 SaTokenContextMockUtil.setMockContext(() - { // StpUtil.setTokenValueToStorage(f452571f-bfdb-413d-aba9-e26992cf07be); // 模拟 Token System.out.println(定时任务Mock 范围内是否登录 StpUtil.isLogin()); // 模拟登录 // StpUtil.login(10066); // 模拟 登录 // System.out.println(定时任务Mock 范围内登录账号 StpUtil.getLoginId()); }); }从上述示例可以看出在定时任务等非 Web 场景中除了 Mock 上下文还可以通过StpUtil.login(10066)直接模拟登录态或通过StpUtil.setTokenValueToStorage(...)模拟指定 Token两种方式都可行。单元测试验证核心类 SaTokenContextMockUtilTest.java 中提供了对应行为的验证用例可帮助你理解其语义有返回值的 Mock 上下文回调应返回结果并清理上下文在setMockContext回调内SaHolder.getRequest()可以正常获取回调结束后再次获取则会抛出RuntimeException证明上下文已被正确清除Mock 响应应保留状态、响应头和重定向目标SaResponseForMock的setStatus、setHeader、addHeader、redirect等操作均会保存到内部字段方便在非 Web 场景下断言响应行为。三、踩坑警示不要用 InheritableThreadLocal 存储上下文在 sa-token-demo-async 的 TestController.java 的末尾注释中官方记录了一个非常典型的踩坑案例值得所有使用者在异步场景下警惕RequestMapping(isLogin2) public SaResult isLogin2() { System.out.println(是否登录 StpUtil.isLogin()); new Thread(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(是否登录 StpUtil.isLogin()); }).start(); return SaResult.data(null); }现象是如果子线程内不Thread.sleep(1000)外面 true里面也 true如果子线程内Thread.sleep(1000)则外面 true里面 false。原因是SpringBoot 会在请求结束后清除 request 里的数据。此时子线程内部虽然可以读取到 request但 request 已经无值导致代码既能成功运行、逻辑又是错误的是一种难以排查的隐形 bug。因此官方明确建议应该避免使用 InheritableThreadLocal 来存储上下文数据。这正是 Sa-Token 在异步场景下需要显式使用SaTokenContextMockUtil手动 Mock 上下文而不是依赖上下文自动向子线程传递的原因。四、响应式上下文WebFlux / Spring Cloud 环境下的处理在 WebFlux / Spring Cloud 等响应式环境下调用 Sa-Token 的同步 API 也有可能发生上下文异常cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化 at cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(SaTokenContextForThreadLocalStaff.java:73) ~[classes/:na] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaReactorFilter [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ HTTP GET /test/isLogin [ExceptionHandlingWebHandler]响应式场景下出现该异常的根本原因与异步场景类似Reactor 的响应式流是异步、非阻塞执行的执行线程可能在多个线程间切换且ServerWebExchange并不存储在当前执行线程的 ThreadLocal 中。因此需要在调用 Sa-Token 同步 API 之前手动绑定上下文。方案一自定义 Filter 中手动 set 上下文如果是在自定义 Filter 中报的这个错需要你在调用 Sa-Token 的同步 API 之前手动 set 一下上下文// 自定义过滤器 Component public class MyFilter implements WebFilter { Override public MonoVoid filter(ServerWebExchange exchange, WebFilterChain chain) { try { // 先 set 上下文再调用 Sa-Token 同步 API并在 finally 里清除上下文 SaReactorSyncHolder.setContext(exchange); System.out.println(StpUtil.isLogin()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } }该示例的完整可运行版本位于 sa-token-demo-webflux-springboot3 的 MyFilter.java。方案二Controller 中两种写法在 Controller 里同理既可以使用 try-finally 手动清除也可以使用带回调的重载方法自动清除RequestMapping(isLogin2) public SaResult isLogin2(ServerWebExchange exchange) { SaResult res SaReactorSyncHolder.setContext(exchange, ()-{ System.out.println(是否登录 StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); }SaReactorSyncHolder的完整源码位于 SaReactorSyncHolder.javasa-token-spring-boot-reactor-v3v4-common模块中另有同一实现。其底层机制为setContext(ServerWebExchange)通过SaStrategy.instance从exchange的 request、response 创建对应的SaRequest、SaResponse、SaStorage再通过SaManager.getSaTokenContext().setContext(...)写入当前线程上下文clearContext()调用上下文管理器的clearContext()清理 ThreadLocalsetContext(exchange, fun)在try-finally中完成写入、执行、清除保证上下文不会泄漏。方案三响应式流中的推荐写法 SaReactorHolder.sync在响应式流的执行过程中例如Mono/Flux链的map、flatMap回调中调用 Sa-Token API 时推荐使用SaReactorHolder.sync(() - { ... })进行包裹。该工具类源码位于 SaReactorHolder.java它基于 Reactor 的deferContextual从 Reactor Context 中取出ServerWebExchangekey 为SA_REACTOR_EXCHANGE_KEY写入同步上下文后执行回调并在 finally 中清除// API测试自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 RequestMapping(isLogin3) public MonoSaResult isLogin3() { return SaReactorHolder.sync(() - { System.out.println(是否登录 StpUtil.isLogin()); userService.isLogin(); return SaResult.data(StpUtil.getTokenInfo()); }); }在更复杂的嵌套调用切换线程、流式延迟场景下规则依然是在流里调用 Sa-Token API 之前必须用SaReactorHolder.sync( () - {} )进行包裹。示例可见 webflux-springboot3 的 TestController.java 中的isLogin5接口// API测试切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 RequestMapping(isLogin5) public MonoSaResult isLogin5() { System.out.println(线程id----- Thread.currentThread().getId()); // 要点在流里调用 Sa-Token API 之前必须用 SaReactorHolder.sync( () - {} ) 进行包裹 return Mono.delay(Duration.ofSeconds(1)) .doOnNext(r- System.out.println(线程id----- Thread.currentThread().getId())) .map(r- SaReactorHolder.sync( () - userService.isLogin() )) .map(r- userService.findUserIdByNamePwd(ZhangSan, 123456)) .map(r- SaReactorHolder.sync( () - userService.isLogin() )) .flatMap(isLogin - { System.out.println(是否登录 isLogin); return SaReactorHolder.sync(() - { System.out.println(是否登录 StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); }); }使用上下文无关 API 的另一种思路在响应式 Controller 中如果只是为了校验某个 Token 是否有效、获取其对应的登录账号还可以使用与上下文无关的 API无需 set 上下文例如StpUtil.getLoginIdByToken(satoken)因为该方法只依赖 Token 字符串本身而非当前请求对象// API测试使用上下文无关的API --- http://localhost:8081/test/isLogin6 RequestMapping(isLogin6) public SaResult isLogin6(CookieValue(satoken) String satoken) { System.out.println(token 为 satoken); System.out.println(登录人 StpUtil.getLoginIdByToken(satoken)); return SaResult.ok(登录人 StpUtil.getLoginIdByToken(satoken)); }这种写法适用于前端显式携带 Token、且只需按 Token 查询的场景可以避免显式管理上下文的负担。五、总结与选型建议场景推荐方案核心 APInew Thread子线程、线程池、Async、Scheduled等非 Web 上下文手动 Mock 上下文SaTokenContextMockUtil.setMockContext(() - { ... })WebFlux 自定义 Filter 中调用同步 API手动 set / clearSaReactorSyncHolder.setContext(exchange)clearContext()WebFlux Controller 中调用同步 API带回调重载自动清理SaReactorSyncHolder.setContext(exchange, () - { ... })响应式流Mono/Flux中嵌套调用流式包装器SaReactorHolder.sync(() - { ... })仅校验 Token、无需请求对象上下文无关 APIStpUtil.getLoginIdByToken(token)需要特别注意的是无论是SaTokenContextMockUtil还是SaReactorSyncHolder其上下文都基于 ThreadLocal 存储。在异步或响应式场景中手动写入上下文后务必在finally中或使用带回调的重载方法清除上下文防止 ThreadLocal 泄漏导致的数据串扰。同时应避免使用InheritableThreadLocal隐式传递上下文以免产生能运行但逻辑错误的隐性 bug。掌握以上方案即可在 Sa-Token 覆盖的绝大多数异步与响应式场景中放心使用权限校验 API。更完整的可运行示例请参考仓库中的 sa-token-demo-async 与 sa-token-demo-webflux-springboot3 两个示例模块。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考