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

Spring OAuth2 Server中,如何用SPA登录页实现PKCE授权码认证?

解决无会话下Spring OAuth2 Server记住PKCE授权请求参数的问题

核心思路

默认Spring OAuth2 Server依赖会话存储授权请求(包含code_challenge等关键参数),无会话场景下需替换授权请求的存储机制,通过Cookie或外部存储(如Redis)关联用户登录前后的授权请求上下文。

方案一:使用Cookie存储授权请求参数

Spring OAuth2 Server内置CookieOAuth2AuthorizationRequestRepository,可将授权请求参数加密后存入Cookie,无需依赖会话。

配置步骤

  1. 在授权服务器的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();
}
  1. 自定义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作为关联标识。

实现步骤

  1. 编写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;
    }
}
  1. 在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();
}

前端配合要点

  1. 跳转到自定义登录页时,从URL或Cookie中提取state参数,存入页面上下文。
  2. 登录验证成功后,重定向回授权服务器的/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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 15:58:16