Spring Webflux Security通过Google OAuth2登录后未附加CSRF Cookie
我正在为Spring Webflux后端和ReactJS前端实现基于Google OAuth2的简易登录功能,目前通过OpenAPI Swagger页面测试API。登录功能已接近完成,但存在一个问题:通过Google登录后,API响应仅附加SESSION Cookie,未附带X-CSRF Cookie。因此,对受保护端点的GET请求可正常工作,但POST请求因缺少可被JS访问的X-CSRF Cookie作为请求头而失败。
具体来说,请求http://localhost:8080/login/oauth2/code/google?state=...&code=...&scope=email+profile+......返回302状态码及以下响应头:
Location: /swagger-ui.html(重定向至Swagger UI页面)Set-Cookie: SESSION=<session-id>(标识已登录)
但没有X-CSRF Token的Set-Cookie头。
令人困惑的是,访问Spring的/logout页面时,响应会通过Set-Cookie附加X-CSRF Token,但这并无实际用处,我希望在设置SESSION Cookie的登录响应中同时返回X-CSRF Cookie。
补充说明:我的Swagger UI在获取到X-CSRF Cookie后可正常工作——先访问/logout页面获取该Cookie,返回Swagger UI后就能正确将其作为请求头使用。
请问我的配置中缺少了什么?以下是我的配置代码:
SecurityConfig.kt
@Configuration @EnableWebFluxSecurity class SecurityConfig { @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http .csrf { csrf -> csrf .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(ServerCsrfTokenRequestAttributeHandler()) } .authorizeExchange { exchange -> exchange .anyExchange() .authenticated() } .oauth2Login(Customizer.withDefaults()) .build() } }
application.yml
springdoc: swagger-ui: enabled: true csrf: enabled: true header-name: "X-XSRF-TOKEN" cookie-name: "XSRF-TOKEN" url: /my-open-api-spec.yaml path: /swagger-ui.html api-docs: enabled: true path: /api-docs spring: security: oauth2: client: registration: google: client-id: ${google_client_id} client-secret: ${google_client_secret} scope: - email - profile
Application.kt
@SpringBootApplication @EnableR2dbcAuditing class MyApplication fun main(args: Array<String>) { runApplication<MyApplication>(*args) }
HttpRouter.kt
@Configuration class HttpRouter(val userHandler: UserHandler) { @Bean fun routes(): RouterFunction<ServerResponse> { return router { "/users".nest { GET("/me", userHandler::getMyUserDetails) POST("", userHandler::inviteUser) GET("", userHandler::searchUsers) } } } }
问题核心是OAuth2登录完成后的302重定向响应中,Spring Security默认不会主动生成并携带CSRF Token Cookie。需要通过自定义认证成功处理器,在登录完成后手动生成并设置CSRF Token到响应Cookie中。
步骤1:自定义OAuth2登录成功处理器
创建CustomOAuth2LoginSuccessHandler类,实现ServerAuthenticationSuccessHandler接口,在登录成功时生成CSRF Token并写入响应Cookie:
import org.springframework.security.core.Authentication import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository import org.springframework.web.server.ServerWebExchange import reactor.core.publisher.Mono class CustomOAuth2LoginSuccessHandler( private val csrfTokenRepository: ServerCsrfTokenRepository ) : ServerAuthenticationSuccessHandler { override fun onAuthenticationSuccess( exchange: ServerWebExchange, authentication: Authentication ): Mono<Void> { return csrfTokenRepository.generateToken(exchange) .flatMap { csrfToken -> csrfTokenRepository.saveToken(exchange, csrfToken) .then(Mono.fromRunnable { exchange.response.addCookie(csrfTokenRepository.toCookie(csrfToken)) }) } .then(Mono.defer { // 保留原重定向逻辑,默认跳转到Swagger UI val redirectUrl = exchange.request.queryParams.getFirst("redirect_uri") ?: "/swagger-ui.html" exchange.response.statusCode = org.springframework.http.HttpStatus.FOUND exchange.response.headers.location = java.net.URI.create(redirectUrl) Mono.empty() }) } }
步骤2:更新SecurityConfig配置
在SecurityConfig中注入CSRF Token仓库实例,并将自定义处理器配置到oauth2Login流程中:
@Configuration @EnableWebFluxSecurity class SecurityConfig { @Bean fun csrfTokenRepository(): ServerCsrfTokenRepository { return CookieServerCsrfTokenRepository.withHttpOnlyFalse() } @Bean fun springSecurityFilterChain( http: ServerHttpSecurity, csrfTokenRepository: ServerCsrfTokenRepository ): SecurityWebFilterChain { val successHandler = CustomOAuth2LoginSuccessHandler(csrfTokenRepository) return http .csrf { csrf -> csrf .csrfTokenRepository(csrfTokenRepository) .csrfTokenRequestHandler(ServerCsrfTokenRequestAttributeHandler()) } .authorizeExchange { exchange -> exchange .anyExchange() .authenticated() } .oauth2Login { oauth2 -> oauth2.authenticationSuccessHandler(successHandler) } .build() } }
关键说明
- 自定义处理器在登录认证完成后,通过
csrfTokenRepository生成并保存CSRF Token,再将Token写入响应Cookie,确保登录后的302响应同时携带SESSION和XSRF-TOKEN。 - 保留了原有的重定向逻辑,确保登录后依然跳转到指定页面(默认Swagger UI)。
- 修改后,前端可直接获取到X-CSRF Cookie,无需额外访问
/logout页面,POST请求就能正常携带CSRF请求头通过校验。
内容的提问来源于stack exchange,提问作者SnoopDougg

