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

Spring Security抛出异常后重定向至formLogin页面问题求助

问题描述

调用注册接口http://localhost:8080/api/v1/registration/register创建用户时,首次调用可成功创建;但重复提交相同用户信息时,服务器日志正常抛出email already taken错误,程序却未返回错误信息,而是重定向至/login端点。

相关代码

WebSecurityConfig 配置

package com.example.venuematchingservice.security.config;

import com.example.venuematchingservice.appuser.AppUserService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@AllArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig {

    private final AppUserService appUserService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf
                        .disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/v1/registration/**", "*").permitAll()
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/js/**", "/images/**");
    }


    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(appUserService);
        authProvider.setPasswordEncoder(bCryptPasswordEncoder);
        return authProvider;
    }
}

AppUserService 类代码

package com.example.venuematchingservice.appuser;

import com.example.venuematchingservice.registration.token.ConfirmationToken;
import com.example.venuematchingservice.registration.token.ConfirmationTokenService;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.UUID;

@Service
@AllArgsConstructor
public class AppUserService implements UserDetailsService {

    private final AppUserRepository appUserRepository;
    private final static String USER_NOT_FOUND = "user with email %s not found";
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private ConfirmationTokenService confirmationTokenService;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return appUserRepository.findByEmail(email).orElseThrow(() ->  new UsernameNotFoundException(String.format(USER_NOT_FOUND, email)));
    }

    public String signUpUser(AppUser appUser){
        boolean userExists = appUserRepository.findByEmail(appUser.getEmail()).isPresent();

        if (userExists){
            throw new IllegalStateException("email already taken");
        }

        String encodedPassword = bCryptPasswordEncoder.encode(appUser.getPassword());
        appUser.setPassword(encodedPassword);
        appUserRepository.save(appUser);

        String token = UUID.randomUUID().toString();
        ConfirmationToken confirmationToken = new ConfirmationToken(
                token,
                LocalDateTime.now(),
                LocalDateTime.now().plusMinutes(15),
                appUser
        );
        confirmationTokenService.saveConfirmationToken(confirmationToken);

        return token;
    }

    public int enableAppUser(String email) {
        return appUserRepository.enableAppUser(email);
    }
}
问题原因及解决办法

原因解析

  1. 异常触发默认错误处理后被拦截:当AppUserService抛出IllegalStateException未被捕获时,Spring Boot会将请求转发到/error端点。但你的Security配置中anyRequest().authenticated()要求所有未明确放行的请求必须认证,而/error不在放行列表里,所以Spring Security会将请求重定向到登录页面。
  2. 请求匹配规则错误:配置里的*只匹配当前路径下的一级资源,无法覆盖/error这类端点,导致异常处理时的请求被拦截。

解决方案

1. 修正Security的请求放行规则

更新WebSecurityConfig中的authorizeHttpRequests配置,明确放行/error端点,同时去掉无效的*匹配:

.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/v1/registration/**", "/error").permitAll()
        .anyRequest().authenticated())

2. 添加全局异常处理器

创建全局异常处理类,直接返回异常信息的HTTP响应,避免触发默认的/error转发:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<String> handleDuplicateEmail(IllegalStateException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
    }
}

3. 适配纯API场景(可选)

如果你的应用是纯API服务,不需要表单登录页面,可以修改formLogin配置,禁用默认登录页面,返回JSON格式的认证错误:

.formLogin(form -> form
        .loginPage("/api/v1/auth/login") // 替换为你的自定义登录API
        .failureHandler((request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"认证失败\"}");
        })
        .permitAll())

内容的提问来源于stack exchange,提问作者Ricardo Blohm

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 06:12:05