Spring Security配置defaultSuccessUrl外部URL引发CORS错误
后端基于Spring Boot + Spring Security运行在localhost:8080,前端React运行在localhost:9000,二者通过HTTP请求通信。已配置Spring Security的CORS规则,前端对后端的GET、POST等请求均正常,但当Spring Security认证成功后重定向到外部URL(如前端首页localhost:9000)时出现CORS错误,重定向到后端本地URL(如/或/bookmarks)则无问题。需求是实现用户在React自定义登录页认证成功后,跳转到React首页。
JS登录请求
前端发送给Spring的模拟登录fetch请求:
const login = async () => { const response = await fetch("http://localhost:8080/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ username: "admin", password: "admin", }), }); return response; }; login() .then((response) => console.log(response)) .catch((err) => console.log(err));
HTTP请求/响应详情
从localhost:8080重定向后向localhost:9000发送的请求:
-- 通用信息 -- 请求URL: http://localhost:9000/ 请求方法: GET 状态码: 304 Not Modified 远程地址: :80 -- 响应头 -- Accept-Ranges: bytes Connection: keep-alive Date: Tue, 02 May 2023 20:29:24 GMT ETag: W/"15d-ORaJAU+aH9t5HZ/clgri7Q6Qsaw" X-Powered-By: Express -- 请求头 -- Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Cookie: JSESSIONID=F37609EF95D9A21AA777DD208F8BF3FF Host: localhost:9000 If-None-Match: W/"15d-ORaJAU+aH9t5HZ/clgri7Q6Qsaw" Origin: null Referer: http://localhost:9000/ sec-ch-ua: "Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
浏览器控制台错误
Access to fetch at 'localhost:9000/' (redirected from 'localhost:8080/login') from origin 'localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Spring Security配置
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain configuration(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .cors(Customizer.withDefaults()) // 请求授权规则 .authorizeHttpRequests(authorize -> authorize .requestMatchers("/login").permitAll() .anyRequest().authenticated()) // 登录页面配置 .formLogin(form -> form .loginPage("http://localhost:9000/auth/login") .loginProcessingUrl("/login") .defaultSuccessUrl("http://localhost:9000", true) .permitAll()) .httpBasic(Customizer.withDefaults()) .build(); } }
CORS配置
@Configuration public class CorsConfig { @Bean CorsConfiguration corsConfiguration() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:9000/")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE")); configuration.setAllowedHeaders(Arrays.asList("Content-Type", "X-XSRF-TOKEN", "Authorization")); configuration.setAllowCredentials(true); return configuration; } @Bean public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", corsConfiguration()); return source; } }
已尝试的方法与推测
- 将CORS配置中的允许源、方法、头设为
*,无效 - 直接从React应用发送GET请求到
localhost:9000无错误 - 问题可能源于认证后响应的Origin头为null,导致浏览器判定跨域
defaultSuccessUrl设为后端本地URL无问题,推测默认认证成功处理器不支持外部URL,需自定义- 前端使用React Router,排除路由问题
- 访问后端受保护端点时可成功重定向到登录页,无错误
解决方案
核心原因
前端通过fetch发送登录请求时,后端返回的重定向响应会被浏览器CORS机制拦截:跨域重定向导致请求的Origin为null,而前端服务器未返回Access-Control-Allow-Origin头,触发CORS错误。
解决步骤
1. 自定义认证成功处理器,返回JSON响应而非重定向
创建自定义处理器,认证成功时返回包含跳转URL的JSON,由前端控制页面跳转:
@Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_OK); // 返回前端跳转地址 String json = "{\"redirectUrl\":\"http://localhost:9000\"}"; response.getWriter().write(json); } }
2. 更新Spring Security配置,替换默认成功处理器
修改SecurityConfig,用自定义处理器替代defaultSuccessUrl:
@Configuration public class SecurityConfig { @Autowired private CustomAuthenticationSuccessHandler successHandler; @Bean public SecurityFilterChain configuration(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .cors(Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/login").permitAll() .anyRequest().authenticated()) .formLogin(form -> form .loginPage("http://localhost:9000/auth/login") .loginProcessingUrl("/login") .successHandler(successHandler) // 使用自定义处理器 .permitAll()) .httpBasic(Customizer.withDefaults()) .build(); } }
3. 前端修改登录逻辑,手动处理页面跳转
更新fetch请求逻辑,拿到后端返回的跳转URL后,通过window.location或React Router跳转:
const login = async () => { try { const response = await fetch("http://localhost:8080/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ username: "admin", password: "admin", }), }); if (response.ok) { const data = await response.json(); // 跳转到前端首页 window.location.href = data.redirectUrl; // 使用React Router可替换为:navigate(data.redirectUrl); } } catch (err) { console.log(err); } }; login();
4. 规范CORS配置的允许源(可选)
移除允许源URL末尾的斜杠,避免匹配问题:
configuration.setAllowedOrigins(Arrays.asList("http://localhost:9000"));
内容的提问来源于stack exchange,提问作者m_rus77

