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

Spring Boot创建重复用户返回403错误而非预期异常问题排查

问题:创建重复邮箱用户时返回403而非预期异常

在Spring Boot项目中,/users端点可正常创建新用户,但当尝试使用已存在的邮箱创建用户时,系统返回403错误,而非预期的带堆栈跟踪的RuntimeException。

相关代码

SecurityFilterChain实现

@EnableWebSecurity
@Configuration
public class WebSecurity  {
    private final UserService userDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurity(UserService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(bCryptPasswordEncoder);

        http
            .csrf(csrf -> csrf.disable())
            .authorizeRequests()
            .requestMatchers(HttpMethod.POST, "/users").permitAll()
            .anyRequest().authenticated();

        return http.build();
    }
}

UserService实现

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private Utils utils;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public UserDto createUser(UserDto userDto) {
        UserEntity exists = userRepository.findByEmail(userDto.getEmail());

        if(exists != null){
            throw new RuntimeException("Record already exists" );
        }

        UserEntity userEntity = new UserEntity();
        BeanUtils.copyProperties(userDto, userEntity);

        String publicUserId = utils.generateUserId(30);
        userEntity.setUserId(publicUserId);
        userEntity.setEncryptedPassword(bCryptPasswordEncoder.encode(userDto.getPassword()));

        UserEntity storedUserEntity = userRepository.save(userEntity);

        UserDto returnValue = new UserDto();
        BeanUtils.copyProperties(storedUserEntity, returnValue);
        return returnValue;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username);
        if(userEntity == null){
            throw new UsernameNotFoundException(username);
        }
        return new User(username, userEntity.getEncryptedPassword(), new ArrayList<>());
    }
}

原因分析

  1. 类职责混淆:UserServiceImpl同时实现了业务接口UserService和Spring Security的UserDetailsService,导致业务异常被Spring Security的认证流程拦截。
  2. 异常拦截机制:当创建重复用户抛出RuntimeException后,Spring Security的默认异常处理器会将其转换为403 Forbidden响应,而非交由Spring MVC的异常处理器返回堆栈信息。
  3. 意外触发认证:即使/users配置了permitAll,如果请求中携带了认证相关的请求头(如Authorization),Spring Security仍会触发认证流程,间接导致异常被拦截。

解决办法

1. 分离业务与认证逻辑

单独实现UserDetailsService,让UserServiceImpl专注于用户业务:

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username);
        if(userEntity == null){
            throw new UsernameNotFoundException(username);
        }
        return new User(username, userEntity.getEncryptedPassword(), new ArrayList<>());
    }
}

修改WebSecurity的依赖注入,使用CustomUserDetailsService替代UserService。

2. 自定义Spring Security异常处理

在SecurityFilterChain配置中添加异常处理器,将业务异常转换为合适的HTTP响应:

http
    .csrf(csrf -> csrf.disable())
    .authorizeRequests()
    .requestMatchers(HttpMethod.POST, "/users").permitAll()
    .anyRequest().authenticated()
    .and()
    .exceptionHandling(exceptions -> exceptions
        .accessDeniedHandler((request, response, accessDeniedException) -> {
            Throwable rootCause = accessDeniedException.getCause();
            if (rootCause instanceof RuntimeException) {
                response.setStatus(HttpServletResponse.SC_CONFLICT);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\": \"" + rootCause.getMessage() + "\"}");
            } else {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
            }
        })
    );

3. 清理请求中的认证头

确保创建用户的POST请求没有携带不必要的Authorization头,避免触发Spring Security的认证流程。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 21:45:28