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

Spring Security+React登录认证异常:错误密码仍跳转至首页

Spring Security + React 登录认证异常:错误密码仍跳转至首页

即便输入错误密码,前端仍跳转至/home页面,Chrome控制台显示请求状态为200。但通过Postman或直接访问localhost:8080/login时认证正常,仅React端(localhost:5173/login)存在该问题。


Spring Security 后端代码

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource){
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);

        jdbcUserDetailsManager.setUsersByUsernameQuery(
                "select user_id,pw,active from members where user_id=?"
        );
        jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
                "select user_id,role from roles where user_id=?"
        );

        return jdbcUserDetailsManager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
                .authorizeHttpRequests(configurer -> {
                    configurer
                            .requestMatchers(HttpMethod.POST,"/login").permitAll()
                            .requestMatchers(HttpMethod.GET,"api/employees").hasRole("EMPLOYEE")
                            .requestMatchers(HttpMethod.GET,"api/employees/**").hasRole("EMPLOYEE")
                            .requestMatchers(HttpMethod.POST,"api/employees").hasRole("MANAGER")
                            .requestMatchers(HttpMethod.PUT,"api/employees/**").hasRole("MANAGER")
                            .requestMatchers(HttpMethod.DELETE,"api/employees").hasRole("ADMIN")
                            .anyRequest().authenticated();
                })
                .formLogin(form -> {
                    form
                            .loginProcessingUrl("/login")
                            .defaultSuccessUrl("/home", true)
                            .failureForwardUrl("/")
                            .permitAll();
                })
                .logout(logout -> {
                    logout
                            .logoutSuccessUrl("/login")
                            .permitAll();
                })
        ;

        //use Http basic authentication
        http.httpBasic(Customizer.withDefaults());

        //disable cross site request forgery(CSFR)
        //not required for stateless rest api that use get,post,put,patch,delete
        http.csrf(csrf -> csrf.disable());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

React 前端登录页面代码

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { Grid, TextField, Button, Typography } from '@mui/material';

const Login = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();

  const handleLogin = async () => {
    try {
      const response = await axios.post('http://localhost:8080/login', new URLSearchParams({
        username: username,
        password: password
      }), { withCredentials: true });
      console.log(response);
      if (response.status === 200) {
        navigate('/home');
      } else {
        console.error('Login failed');
      }
    } catch (error) {
      console.error('Login failed:', error.message);
    }
  };

  return (
    <Grid container spacing={2} justifyContent="center" alignItems="center" style={{ minHeight: '100vh' }}>
      <Grid item xs={12} sm={6}>
        <Typography component="h1" variant="h5" align="center">
          Sign in
        </Typography>
        <form>
          <Grid container spacing={2} justifyContent="center" alignItems="center">
            <Grid item xs={12}>
              <TextField
                variant="outlined"
                margin="normal"
                required
                fullWidth
                id="username"
                label="Username"
                name="username"
                autoComplete="username"
                autoFocus
                value={username}
                onChange={(e) => setUsername(e.target.value)}
              />
            </Grid>
            <Grid item xs={12}>
              <TextField
                variant="outlined"
                margin="normal"
                required
                fullWidth
                name="password"
                label="Password"
                type="password"
                id="password"
                autoComplete="current-password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
            </Grid>
            <Grid item xs={12}>
              <Button
                type="button"
                fullWidth
                variant="contained"
                color="primary"
                onClick={handleLogin}
              >
                Sign In
              </Button>
            </Grid>
          </Grid>
        </form>
      </Grid>
    </Grid>
  );
};

export default Login;

问题原因

  1. 后端formLogin逻辑不适配API请求:当前配置的failureForwardUrl("/")是针对服务器端渲染的页面跳转,登录失败时Spring Security会返回200状态码并转发到首页内容,而非返回401等错误状态码,导致前端误判请求成功。
  2. 前端仅依赖状态码判断结果:React代码只要检测到响应状态码为200就跳转,忽略了登录失败时后端返回的实际内容。

解决方案

后端调整(适配API响应)

修改SecurityConfig中的formLogin配置,手动处理成功/失败的响应,返回明确的状态码和JSON数据:

.formLogin(form -> {
    form
            .loginProcessingUrl("/login")
            .successHandler((request, response, authentication) -> {
                // 登录成功返回200和成功标识
                response.setStatus(HttpServletResponse.SC_OK);
                response.setContentType("application/json");
                response.getWriter().write("{\"success\": true}");
                response.getWriter().flush();
            })
            .failureHandler((request, response, exception) -> {
                // 登录失败返回401和错误信息
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json");
                response.getWriter().write("{\"success\": false, \"message\": \"用户名或密码错误\"}");
                response.getWriter().flush();
            })
            .permitAll();
})

同时移除defaultSuccessUrl和failureForwardUrl配置。

前端调整(正确解析响应)

修改handleLogin函数,根据后端返回的success字段判断登录结果,而非仅依赖状态码:

const handleLogin = async () => {
  try {
    const response = await axios.post('http://localhost:8080/login', new URLSearchParams({
      username: username,
      password: password
    }), { withCredentials: true });
    
    if (response.data.success) {
      navigate('/home');
    } else {
      console.error('Login failed:', response.data.message);
      // 可在此添加用户可见的错误提示,比如弹窗或文字提示
    }
  } catch (error) {
    console.error('Login failed:', error.response?.data?.message || error.message);
  }
};

补充:配置CORS(避免跨域问题)

在Spring Boot中添加CORS配置,允许React域名访问:

@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("http://localhost:5173");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

内容的提问来源于stack exchange,提问作者Prashant Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 14:52:02