Spring Boot 3.1.2+OAuth2快速多标签页打开时认证失败问题咨询
核心原因
快速连续打开多标签页时,多个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
相关产品推荐
相关产品推荐

