Spring Boot Security自定义登录页认证遇CORS错误求助
问题分析与解决方案
问题根源
- 跨域预请求(OPTIONS)禁止重定向:浏览器发送跨域预验证请求时,Spring Security的传统表单认证流程触发了重定向(比如未认证跳转至
/login),但预请求的响应不允许是重定向,直接触发CORS拦截。 - 表单认证不兼容JSON请求:你的前端用JSON格式提交用户名密码,但Spring Security默认的
formLogin只处理application/x-www-form-urlencoded格式的表单数据,无法解析JSON参数,导致认证失败并触发重定向。 - 重定向机制不适配前后端分离:
defaultSuccessUrl和failureUrl的跳转逻辑是为服务端渲染页面设计的,前后端分离场景下应返回JSON结果而非页面跳转。
解决步骤
1. 重构Spring Security配置,禁用传统表单认证
放弃formLogin的重定向逻辑,改用REST风格的认证接口,避免跨域预请求冲突:
package com.example.backend.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource; import java.util.Arrays; import java.util.Collections; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/login", "/resources/**", "/static/**").permitAll() .anyRequest().authenticated() ) // 禁用默认表单登录和HTTP Basic认证 .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()) // 按需配置会话管理(保持会话则用IF_REQUIRED,无状态用STATELESS) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ); return http.build(); } @Bean public UserDetailsService userDetailsService() { var user = User.withUsername("user") .password("{noop}password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); configuration.setAllowCredentials(true); // 暴露会话Cookie相关响应头,让前端能获取会话标识 configuration.setExposedHeaders(Arrays.asList("Set-Cookie")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } // 注册AuthenticationManager,供自定义登录接口调用 @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } }
2. 编写自定义JSON登录接口
创建处理JSON请求的登录接口,直接返回JSON结果,避免重定向:
package com.example.backend.controller; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class AuthController { private final AuthenticationManager authenticationManager; public AuthController(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { try { // 构造认证请求 Authentication authRequest = new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ); // 执行认证逻辑 Authentication authenticated = authenticationManager.authenticate(authRequest); // 将认证信息存入上下文 SecurityContextHolder.getContext().setAuthentication(authenticated); return ResponseEntity.ok(new LoginResponse(true, "登录成功", authenticated.getName())); } catch (BadCredentialsException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new LoginResponse(false, "用户名或密码错误", null)); } } // 请求DTO public static class LoginRequest { private String username; private String password; // Getter & Setter public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } // 响应DTO public static class LoginResponse { private boolean success; private String message; private String username; public LoginResponse(boolean success, String message, String username) { this.success = success; this.message = message; this.username = username; } // Getter public boolean isSuccess() { return success; } public String getMessage() { return message; } public String getUsername() { return username; } } }
3. 调整前端请求逻辑
确保React请求携带凭证(会话Cookie),并发送正确的JSON格式数据:
interface LoginParams { username: string; password: string; } interface LoginResponse { success: boolean; message: string; username?: string; } const login = async (params: LoginParams) => { try { const response = await fetch('http://localhost:8080/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', // 必须设置,否则不会携带JSESSIONID Cookie body: JSON.stringify(params), }); const data: LoginResponse = await response.json(); if (response.ok) { console.log('登录成功', data); // 处理登录后的业务逻辑(如跳转首页、存储用户信息) } else { console.error('登录失败', data.message); } } catch (error) { console.error('请求异常', error); } };
额外注意事项
- 若后续采用JWT认证,可在登录接口生成JWT令牌返回给前端,前端后续请求携带
Authorization: Bearer <token>头,并在Spring Security中配置JWT过滤器。 - 确保Spring Boot配置文件中未禁用会话:
server.servlet.session.enabled=true(默认启用)。 - 测试时可先用Postman验证登录接口的正确性,再联调前端。
内容的提问来源于stack exchange,提问作者Zo Ed
相关产品推荐
相关产品推荐

