设置STATELESS后OAuth2授权请求仍带JSESSIONID Cookie问题咨询
我已将Spring Security的SessionCreationPolicy设置为STATELESS,期望所有请求场景都不会返回Set-Cookie头,但在OAuth2流程正常运行的情况下,http://authorize-server.com/...authorize的302响应仍带有JSESSIONID Cookie。我计划通过ResourceServer+JWT实现认证,后续流程无需会话,有两个疑问:
- 若OAuth2流程确实需要
JSESSIONID,是否需要在/api/oauth2/endpoint手动结束会话? - OAuth2流程中的
state参数已可确定请求来源,为何还要使用Cookie+JSESSIONID?
1. STATELESS下仍出现JSESSIONID的原因
尽管配置了SessionCreationPolicy.STATELESS,但OAuth2 Login流程默认依赖服务器端会话存储授权请求的上下文信息——包括state、nonce(OpenID Connect场景)、客户端ID、重定向URI等元数据。在重定向到授权服务器前,Spring Security会将这些信息存入会话,因此会生成JSESSIONID Cookie。
即使配置了NullSecurityContextRepository,OAuth2 Login的核心过滤器OAuth2AuthorizationRequestRedirectFilter仍会使用会话保存授权请求,这是默认实现的固有行为。
2. 是否需要手动结束会话?
如果后续完全基于JWT运行、不需要服务器端会话,建议在OAuth2登录完成的回调端点/api/oauth2/endpoint手动销毁会话,避免无用Cookie残留。有两种实现方式:
方式一:在回调接口中添加销毁逻辑
@GetMapping("/api/oauth2/endpoint") fun callback(request: HttpServletRequest, response: HttpServletResponse): String { // 处理登录后的业务逻辑(比如生成JWT返回给前端) // 销毁服务器端会话 request.session.invalidate() // 清除客户端JSESSIONID Cookie val cookie = Cookie("JSESSIONID", null) cookie.path = "/" cookie.maxAge = 0 response.addCookie(cookie) return "登录成功" }
方式二:通过Spring Security登录成功处理器配置
.oauth2Login { // 其他原有配置... it.successHandler { request, response, authentication -> // 销毁会话 request.session.invalidate() // 清除Cookie val cookie = Cookie("JSESSIONID", null) cookie.path = "/" cookie.maxAge = 0 response.addCookie(cookie) // 跳转到目标页面或返回响应 response.sendRedirect("/api/oauth2/endpoint") } }
3. state参数与Cookie+JSESSIONID的区别
state参数的核心作用是防止CSRF攻击,仅用于验证回调请求与发起的授权请求是否匹配,但它不存储完整的授权上下文信息。而Cookie+JSESSIONID的作用是:
- 存储授权请求的完整元数据(如客户端ID、scope、重定向URI等),回调时需要这些信息匹配
state并向授权服务器请求令牌; - 避免敏感上下文通过URL传递(URL长度有限且易被日志记录);
- OpenID Connect场景下,还会存储
nonce参数防止ID令牌重放攻击。
两者职责不同,state是短验证字符串,会话是上下文存储容器,无法互相替代。
4. 进阶优化:让OAuth2 Login摆脱服务器端会话
如果想彻底避免生成JSESSIONID,可以自定义OAuth2AuthorizationRequestRepository,将授权上下文存储到前端加密Cookie中,而非服务器端会话。示例实现:
class CookieOAuth2AuthorizationRequestRepository : OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> { private val AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request" private val STATE_PARAM_COOKIE_NAME = "oauth2_state" private val cookieExpireSeconds = 180 override fun loadAuthorizationRequest(request: HttpServletRequest): OAuth2AuthorizationRequest? { return getCookie(request, AUTHORIZATION_REQUEST_COOKIE_NAME) ?.let { cookie -> ObjectMapper().readValue(URLDecoder.decode(cookie.value, StandardCharsets.UTF_8), OAuth2AuthorizationRequest::class.java) } } override fun saveAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest?, request: HttpServletRequest, response: HttpServletResponse) { if (authorizationRequest == null) { clearCookies(request, response) return } val cookieValue = URLEncoder.encode(ObjectMapper().writeValueAsString(authorizationRequest), StandardCharsets.UTF_8) val authCookie = Cookie(AUTHORIZATION_REQUEST_COOKIE_NAME, cookieValue) authCookie.path = "/" authCookie.maxAge = cookieExpireSeconds authCookie.isHttpOnly = true authCookie.secure = request.isSecure val stateCookie = Cookie(STATE_PARAM_COOKIE_NAME, authorizationRequest.state) stateCookie.path = "/" stateCookie.maxAge = cookieExpireSeconds stateCookie.isHttpOnly = true stateCookie.secure = request.isSecure response.addCookie(authCookie) response.addCookie(stateCookie) } override fun removeAuthorizationRequest(request: HttpServletRequest, response: HttpServletResponse): OAuth2AuthorizationRequest? { return loadAuthorizationRequest(request).also { clearCookies(request, response) } } private fun clearCookies(request: HttpServletRequest, response: HttpServletResponse) { listOf(AUTHORIZATION_REQUEST_COOKIE_NAME, STATE_PARAM_COOKIE_NAME).forEach { cookieName -> getCookie(request, cookieName)?.let { it.maxAge = 0 it.path = "/" response.addCookie(it) } } } private fun getCookie(request: HttpServletRequest, name: String): Cookie? { return request.cookies?.firstOrNull { it.name == name } } }
然后在SecurityFilterChain中配置使用该Repository:
.oauth2Login { // 其他原有配置... it.authorizationEndpoint { it.authorizationRequestRepository(CookieOAuth2AuthorizationRequestRepository()) } }
配置后Spring Security将不再创建服务器端会话,而是通过加密Cookie存储授权上下文,也就不会生成JSESSIONID了。
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http .csrf { it.disable() } .formLogin { it.disable() } .logout { it.disable() } .httpBasic { it.disable() } .anonymous { it.disable() } .oauth2ResourceServer { it.jwt { } } .cors { } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .requestCache { it.requestCache(NullRequestCache()) } .securityContext { it.securityContextRepository(NullSecurityContextRepository()) it.requireExplicitSave(true) } .authorizeHttpRequests { it.requestMatchers("/api/oauth2/endpoint").permitAll() it.requestMatchers("/api/ping").permitAll() it.anyRequest().authenticated() } .oauth2Login { it.authorizationEndpoint { it.authorizationRequestResolver( oAuth2AuthorizationRequestResolver( registrationRepository, oAuth2AuthorizationRequestCustomizer ) ) } it.tokenEndpoint { it.accessTokenResponseClient( oAuth2AccessTokenResponseClient( oAuth2AuthorizationCodeGrantRequestEntityConverter, mapOAuth2AccessTokenResponseConverter ) ) } it.userInfoEndpoint { it.userService(oAuth2UserService) } it.defaultSuccessUrl("/api/oauth2/endpoint", false) it.failureHandler { request, response, exception -> exception.printStackTrace() } } return http.build() }
@Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOriginPatterns = mutableListOf("*") configuration.allowedMethods = mutableListOf("*") configuration.allowedHeaders = mutableListOf("*") configuration.allowCredentials = true val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) return source } @Bean fun decoder(): JwtDecoder { val originalKey = "b0f29fc0d32efdbabff03d4aae352b4936e69b0c3c6b8a0b067ae2453f96b431".toByteArray() val secretKeySpec = SecretKeySpec(originalKey, "HmacSHA256") return NimbusJwtDecoder.withSecretKey(secretKeySpec).build() } @Bean fun encoder(): JwtEncoder { val originalKey = "b0f29fc0d32efdbabff03d4aae352b4936e69b0c3c6b8a0b067ae2453f96b431".toByteArray() val secretKeySpec = SecretKeySpec(originalKey, "HmacSHA256") return NimbusJwtEncoder(ImmutableSecret(secretKeySpec)) } @Bean fun authenticationConverter(): JwtAuthenticationConverter { val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() grantedAuthoritiesConverter.setAuthorityPrefix("") val authenticationConverter = JwtAuthenticationConverter() authenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) return authenticationConverter } // @Bean fun oAuth2AuthorizationRequestResolver( clientRegistrationRepository: ClientRegistrationRepository, oAuth2AuthorizationRequestCustomizer: OAuth2AuthorizationRequestCustomizer ): OAuth2AuthorizationRequestResolver { val resolver = DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI) resolver.setAuthorizationRequestCustomizer(oAuth2AuthorizationRequestCustomizer) return resolver } // @Bean fun oAuth2AccessTokenResponseClient( oAuth2AuthorizationCodeGrantRequestEntityConverter: OAuth2AuthorizationCodeGrantRequestEntityConverter, mapOAuth2AccessTokenResponseConverter: MapOAuth2AccessTokenResponseConverter ): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> { val authorizationCodeTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() authorizationCodeTokenResponseClient.setRequestEntityConverter(oAuth2AuthorizationCodeGrantRequestEntityConverter) val tokenResponseHttpMessageConverter = OAuth2AccessTokenResponseHttpMessageConverter() tokenResponseHttpMessageConverter.supportedMediaTypes = listOf(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN) tokenResponseHttpMessageConverter.setAccessTokenResponseConverter(mapOAuth2AccessTokenResponseConverter) val restTemplate = RestTemplate(listOf(FormHttpMessageConverter(), tokenResponseHttpMessageConverter)) authorizationCodeTokenResponseClient.setRestOperations(restTemplate) return authorizationCodeTokenResponseClient }
内容的提问来源于stack exchange,提问作者BAHELAN

