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

Spring Boot Security自定义登录页认证遇CORS错误求助

问题分析与解决方案

问题根源

  1. 跨域预请求(OPTIONS)禁止重定向:浏览器发送跨域预验证请求时,Spring Security的传统表单认证流程触发了重定向(比如未认证跳转至/login),但预请求的响应不允许是重定向,直接触发CORS拦截。
  2. 表单认证不兼容JSON请求:你的前端用JSON格式提交用户名密码,但Spring Security默认的formLogin只处理application/x-www-form-urlencoded格式的表单数据,无法解析JSON参数,导致认证失败并触发重定向。
  3. 重定向机制不适配前后端分离: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 18:05:56