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

Spring Boot 3.1.2+OAuth2快速多标签页打开时认证失败问题咨询

解决Spring Boot OAuth2多标签页快速认证的authorization_request_not_found问题

核心原因

快速连续打开多标签页时,多个OAuth2授权请求会共享同一个HttpSession,后续请求会覆盖Session中存储的AuthorizationRequest对象,导致先发起的请求完成回调时找不到对应的授权记录,触发authorization_request_not_found错误,进而跳转至错误页或登出页面。

解决方案

1. 自定义授权请求存储,避免Session覆盖

Spring Security OAuth2默认使用HttpSessionOAuth2AuthorizationRequestRepository存储授权请求,会导致Session内的请求被覆盖。可以实现OAuth2AuthorizationRequestRepository接口,改用请求参数+Cookie的方式存储授权请求,让每个请求的授权记录独立:

import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Base64;

@Component
public class CookieOAuth2AuthorizationRequestRepository implements OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "OAUTH2_AUTH_REQUEST";
    private static final String REDIRECT_URI_PARAM_COOKIE_NAME = "REDIRECT_URI";
    private static final int COOKIE_EXPIRE_SECONDS = 180;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        if (cookie == null) {
            return null;
        }
        return deserialize(cookie.getValue());
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequestCookies(request, response);
            return;
        }
        // 存储授权请求到Cookie
        Cookie authRequestCookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, serialize(authorizationRequest));
        authRequestCookie.setPath("/");
        authRequestCookie.setHttpOnly(true);
        authRequestCookie.setMaxAge(COOKIE_EXPIRE_SECONDS);
        response.addCookie(authRequestCookie);

        // 存储跳转URI到Cookie
        String redirectUri = request.getParameter("redirect_uri");
        if (redirectUri != null) {
            Cookie redirectUriCookie = new Cookie(REDIRECT_URI_PARAM_COOKIE_NAME, redirectUri);
            redirectUriCookie.setPath("/");
            redirectUriCookie.setHttpOnly(true);
            redirectUriCookie.setMaxAge(COOKIE_EXPIRE_SECONDS);
            response.addCookie(redirectUriCookie);
        }
    }

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
        OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request);
        removeAuthorizationRequestCookies(request, response);
        return authRequest;
    }

    private String serialize(OAuth2AuthorizationRequest authorizationRequest) {
        try {
            return Base64.getUrlEncoder().encodeToString(objectMapper.writeValueAsBytes(authorizationRequest));
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to serialize authorization request", e);
        }
    }

    private OAuth2AuthorizationRequest deserialize(String value) {
        try {
            return objectMapper.readValue(Base64.getUrlDecoder().decode(value), OAuth2AuthorizationRequest.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to deserialize authorization request", e);
        }
    }

    private void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
        Cookie authRequestCookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, null);
        authRequestCookie.setPath("/");
        authRequestCookie.setMaxAge(0);
        response.addCookie(authRequestCookie);

        Cookie redirectUriCookie = new Cookie(REDIRECT_URI_PARAM_COOKIE_NAME, null);
        redirectUriCookie.setPath("/");
        redirectUriCookie.setMaxAge(0);
        response.addCookie(redirectUriCookie);
    }
}

然后在Security配置中注册这个自定义Repository:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public SecurityConfig(CookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.authorizationRequestRepository = authorizationRequestRepository;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .authorizationEndpoint(endpoint -> endpoint
                                .authorizationRequestRepository(authorizationRequestRepository)
                        )
                );
        return http.build();
    }
}

2. 前端拦截重复授权请求

在前端页面添加逻辑,检测到正在进行OAuth2授权流程时,阻止新的请求发起,避免冲突:

let isAuthenticating = false;

function triggerAuth() {
    if (isAuthenticating) return;
    isAuthenticating = true;
    window.location.href = "/oauth2/authorization/your-provider-id";
    // 超时重置,防止异常导致锁死
    setTimeout(() => isAuthenticating = false, 10000);
}

3. 错误页兜底处理

为/login?error页面添加重定向逻辑,引导用户重新触发授权流程:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginErrorController {

    @GetMapping("/login")
    public String handleLoginError() {
        // 可添加错误日志记录,再重定向到授权入口
        return "redirect:/oauth2/authorization/your-provider-id";
    }
}

内容的提问来源于stack exchange,提问作者Prudvinath

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 01:02:12