You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 23:38:12