Spring OAuth2 Server中,如何用SPA登录页实现PKCE授权码认证?
解决无会话下Spring OAuth2 Server记住PKCE授权请求参数的问题
核心思路
默认Spring OAuth2 Server依赖会话存储授权请求(包含code_challenge等关键参数),无会话场景下需替换授权请求的存储机制,通过Cookie或外部存储(如Redis)关联用户登录前后的授权请求上下文。
方案一:使用Cookie存储授权请求参数
Spring OAuth2 Server内置CookieOAuth2AuthorizationRequestRepository,可将授权请求参数加密后存入Cookie,无需依赖会话。
配置步骤
- 在授权服务器的Security配置中替换默认授权请求仓库:
@Bean public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http .oauth2Login(oauth2Login -> oauth2Login // 指定自定义登录页地址,需携带授权请求的state参数 .loginPage("/custom-login") ) .oauth2AuthorizationServer(oauth2 -> oauth2 .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint // 配置基于Cookie的授权请求存储 .authorizationRequestRepository(new CookieOAuth2AuthorizationRequestRepository()) ) ); return http.build(); }
- 自定义Cookie安全属性(可选)
可调整Cookie的过期时间、安全属性增强防护:
@Bean public CookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() { CookieOAuth2AuthorizationRequestRepository repository = new CookieOAuth2AuthorizationRequestRepository(); repository.setCookieName("AUTH_REQUEST"); repository.setCookieMaxAge(300); // 5分钟过期 repository.setCookieHttpOnly(true); // 防止XSS攻击 repository.setCookieSecure(true); // 仅HTTPS传输 repository.setCookieSameSite("Lax"); // 适配跨站场景 return repository; }
将上述自定义Bean注入authorizationEndpoint的authorizationRequestRepository即可。
方案二:自定义Redis存储授权请求参数
若Cookie不满足跨域或复杂场景需求,可实现OAuth2AuthorizationRequestRepository接口,用Redis存储授权请求,通过state作为关联标识。
实现步骤
- 编写Redis版授权请求仓库:
@Component public class RedisOAuth2AuthorizationRequestRepository implements OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> { private static final String REDIS_KEY_PREFIX = "oauth2:auth-request:"; private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; public RedisOAuth2AuthorizationRequestRepository(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) { this.stringRedisTemplate = stringRedisTemplate; this.objectMapper = objectMapper; } @Override public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { String state = request.getParameter(OAuth2ParameterNames.STATE); if (state == null) return null; String json = stringRedisTemplate.opsForValue().get(REDIS_KEY_PREFIX + state); if (json == null) return null; try { return objectMapper.readValue(json, OAuth2AuthorizationRequest.class); } catch (JsonProcessingException e) { stringRedisTemplate.delete(REDIS_KEY_PREFIX + state); return null; } } @Override public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { String state = authorizationRequest.getState(); if (state == null) return; try { String json = objectMapper.writeValueAsString(authorizationRequest); stringRedisTemplate.opsForValue().set(REDIS_KEY_PREFIX + state, json, 300, TimeUnit.SECONDS); // 将state存入Cookie,供登录页获取 Cookie cookie = new Cookie("AUTH_STATE", state); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setMaxAge(300); cookie.setPath("/"); response.addCookie(cookie); } catch (JsonProcessingException e) { // 处理序列化异常 } } @Override public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { OAuth2AuthorizationRequest requestObj = loadAuthorizationRequest(request); if (requestObj != null) { stringRedisTemplate.delete(REDIS_KEY_PREFIX + requestObj.getState()); // 清除关联Cookie Cookie cookie = new Cookie("AUTH_STATE", null); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setMaxAge(0); cookie.setPath("/"); response.addCookie(cookie); } return requestObj; } }
- 在Security配置中引用自定义仓库:
@Bean public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, RedisOAuth2AuthorizationRequestRepository authorizationRequestRepository) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http .oauth2Login(oauth2Login -> oauth2Login .loginPage("/custom-login") ) .oauth2AuthorizationServer(oauth2 -> oauth2 .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint .authorizationRequestRepository(authorizationRequestRepository) ) ); return http.build(); }
前端配合要点
- 跳转到自定义登录页时,从URL或Cookie中提取
state参数,存入页面上下文。 - 登录验证成功后,重定向回授权服务器的
/oauth2/authorize端点并携带state参数,授权服务器将通过该参数匹配到对应的授权请求,继续完成PKCE流程。
示例Vue3跳转逻辑:
// 从URL参数获取state const state = new URLSearchParams(window.location.search).get('state'); // 登录成功后重定向 window.location.href = `/oauth2/authorize?state=${state}`;
内容的提问来源于stack exchange,提问作者adddd
相关产品推荐
相关产品推荐

