React+Spring Security跨域登录请求失败问题排查
问题分析与解决方案
核心问题:请求体格式不匹配
Spring Security的formLogin默认期望接收**application/x-www-form-urlencoded类型**的表单数据(即username=xxx&password=xxx格式),但你的前端请求用了application/json格式,并且直接传递JSON对象作为请求体,导致Spring无法正确解析用户名和密码参数,因此认证失败,触发默认的错误跳转/login?error。
解决方案一:修改前端请求格式
将fetch请求改为发送表单格式数据:
fetch('http://localhost:8081/login-process', { credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: "POST", body: new URLSearchParams({ "username": "user", "password": "d4ddeb71-c76b-43f3-a0af-2f3c0b36bbec" }) }) .then(response => response.json()) .then((json) => console.log(json)) .catch(err => console.error(err));
解决方案二:后端配置支持JSON登录
如果希望保持JSON格式请求,可以自定义Spring Security的认证过滤器,解析JSON请求体中的用户名和密码:
- 自定义认证过滤器:
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { try { ObjectMapper mapper = new ObjectMapper(); LoginRequest loginRequest = mapper.readValue(request.getInputStream(), LoginRequest.class); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword()); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } catch (IOException e) { throw new AuthenticationServiceException("解析登录请求失败", e); } } return super.attemptAuthentication(request, response); } // 定义登录请求的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; } } }
- 在Security配置中替换默认过滤器:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.addFilter(corsFilter); // 替换默认的UsernamePasswordAuthenticationFilter JsonUsernamePasswordAuthenticationFilter jsonFilter = new JsonUsernamePasswordAuthenticationFilter(); jsonFilter.setAuthenticationManager(authenticationManagerBean()); jsonFilter.setLoginProcessingUrl("/login-process"); // 配置成功/失败处理器,返回JSON响应(适配前后端分离) jsonFilter.setSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}"); }); jsonFilter.setFailureHandler((request, response, exception) -> { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"登录失败:" + exception.getMessage() + "\"}"); }); http.addFilterAt(jsonFilter, UsernamePasswordAuthenticationFilter.class); http.authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") .anyRequest().permitAll(); }
额外优化:适配前后端分离的登录响应
原配置中的defaultSuccessUrl("/", false)会触发页面跳转,不适合前后端分离场景。建议配置自定义的successHandler和failureHandler,返回JSON格式的响应,方便前端处理登录结果(如上述解决方案二中的代码)。
注意事项
确保你的CORS配置正确:
- 允许的Origin包含
http://localhost:3000 - 允许携带凭证(
allowCredentials(true)) - 允许的HTTP方法包含
POST - 允许的请求头包含
Content-Type
内容的提问来源于stack exchange,提问作者Gukjin
相关产品推荐
相关产品推荐

