Spring WebFlux+Kotlin协程:请求上下文存储与WebSession获取问题
问题分析
你尝试通过CoWebFilter将WebSession存入CoroutineContext,但后续RequestLogFilter无法获取到上下文,核心原因是Spring WebFlux的Filter执行链中,协程上下文没有被正确传递到后续Filter的执行环境中。
解决方案
以下提供两种可行方案,可根据需求选择:
方案一:用ThreadLocal实现全局Session Holder(推荐)
这种方式完全模拟SecurityContextHolder的使用体验,无需传递ServerWebExchange或WebSession即可在任意位置获取当前会话:
1. 定义全局Session Holder
object WebSessionHolder { private val sessionThreadLocal = ThreadLocal<WebSession>() /** * 设置当前请求的WebSession */ fun setSession(session: WebSession) { sessionThreadLocal.set(session) } /** * 获取当前请求的WebSession,无则返回null */ fun getSession(): WebSession? { return sessionThreadLocal.get() } /** * 请求结束后清理Session,避免内存泄漏 */ fun clearSession() { sessionThreadLocal.remove() } }
2. 修改AuthenticationFilter存入Session
@Component @Order(-1) private class AuthenticationFilter( private val adminService: AdminService, private val authService: AuthService, ) : CoWebFilter() { override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) { val session = exchange.session.awaitSingle() try { WebSessionHolder.setSession(session) chain.filter(exchange) } finally { // 必须在finally中清理,防止ThreadLocal内存泄漏 WebSessionHolder.clearSession() } } }
3. 在任意位置获取Session
比如在RequestLogFilter中直接调用:
private class RequestLogFilter(private val requestLogRepo: RequestLogRepo) : CoWebFilter() { // ... 原有代码不变 override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain): Unit = coroutineScope { val cachedRequestBody = exchange.cacheReqBody() val mutatedExchange = exchange.mutateWithCachedReqBody(cachedRequestBody) // 直接从Holder获取当前Session val currentSession = WebSessionHolder.getSession() log.debug { "requestlog 当前SessionID: ${currentSession?.id}" } // ... 原有业务逻辑不变 } }
注意:Kotlin协程会自动处理ThreadLocal的跨线程传递,即使协程切换线程,Session值也会被正确复制,无需额外配置。
方案二:修复CoroutineContext传递问题
如果坚持使用CoroutineContext方式,需要手动在后续Filter中绑定上下文:
1. 修改AuthenticationFilter将Session存入Exchange属性
@Component @Order(-1) private class AuthenticationFilter( private val adminService: AdminService, private val authService: AuthService, ) : CoWebFilter() { companion object { // 定义Exchange属性的Key const val WEB_SESSION_ATTR_KEY = "CURRENT_WEB_SESSION" } override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) { val session = exchange.session.awaitSingle() // 将Session存入Exchange属性 exchange.attributes[WEB_SESSION_ATTR_KEY] = session chain.filter(exchange) } }
2. 在RequestLogFilter中绑定上下文
private class RequestLogFilter(private val requestLogRepo: RequestLogRepo) : CoWebFilter() { // ... 原有代码不变 override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain): Unit = coroutineScope { val cachedRequestBody = exchange.cacheReqBody() val mutatedExchange = exchange.mutateWithCachedReqBody(cachedRequestBody) // 从Exchange属性取出Session并绑定到当前协程上下文 val session = exchange.attributes[AuthenticationFilter.WEB_SESSION_ATTR_KEY] as? WebSession val targetContext = session?.let { coroutineContext + WebSessionContext(it) } ?: coroutineContext withContext(targetContext) { log.debug { "requestlog ${coroutineContext[WebSessionContext.Key]}" } // 所有需要上下文的业务逻辑都放在withContext块内 with(mutatedExchange.request) { val method = method.toString() val path = path.toString() val params = queryParams.toString() val body = DataBufferUtils.join(cachedRequestBody).awaitSingleOrNull()?.toString(Charset.defaultCharset()) log.debug { "$method $path 请求开始, params: $params body: $body" } val time = measureTimeMillis { chain.filter(mutatedExchange) } log.info { "$method $path 请求, 耗时: ${time}ms" } launch { RequestLog(method, path, params, body, time).let(requestLogRepo::save) } } } } }
这种方式需要在每个需要上下文的Filter中手动绑定,相对繁琐,但保持了CoroutineContext的使用方式。
内容的提问来源于stack exchange,提问作者BATTLEHAWK
相关产品推荐
相关产品推荐

