Spring Security注册自动登录authenticationManager返回401问题
问题现象
- 现有项目已完成注册、登录接口开发,登录接口运行正常,用户登录后可正常签发JWT令牌
- 预期实现效果:用户注册成功后自动完成认证,直接返回JWT令牌,无需二次登录
- 实际异常表现:前端提交
UserDto参数后,用户数据可正常写入数据库,但认证流程阻塞,代码停在SignupController中如下位置,无自定义业务异常抛出:
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
- Postman调试接口直接返回
401 Unauthorized响应
核心参考代码
SecurityConfiguration.java
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration { @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private JwtRequestFilter jwtRequestFilter; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*") .permitAll() .antMatchers("/flightBooking", "/flightBooking/signup", "/flightBooking/login") .permitAll() .anyRequest() .authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }
LoginController.java
@RestController @RequestMapping("/flightBooking/login") public class LoginController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private FlightBookingUserDetailsService flightBookingUserDetailsService; @PostMapping public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthReq authReq) throws Exception { this.authenticate(authReq.getUsername(), authReq.getPassword()); final UserDetails userDetails = flightBookingUserDetailsService.loadUserByUsername(authReq.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return ResponseEntity.ok(new AuthRes(token)); } private void authenticate(String username, String password) throws Exception { try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { throw new Exception("INVALID_EXCEPTION", e); } } }
SignupController.java
@RestController @RequestMapping("/flightBooking/signup") public class SignupController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private FlightBookingUserDetailsService flightBookingUserDetailsService; @Autowired private IUserService iUserService; @Autowired private UserMapper mapper; @PostMapping public ResponseEntity<?> signUp(@RequestBody UserDto userDto) throws Exception { userDto.setRoleId(2L); Long userId = this.iUserService.saveEntity(this.mapper.map(userDto)); if (userId > 0) { this.authenticate(userDto.getUsername(), userDto.getUserPass()); final UserDetails userDetails = flightBookingUserDetailsService.loadUserByUsername(userDto.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); AuthRes res = new AuthRes(token); res.setId(userId); return ResponseEntity.ok(res); } return null; } private void authenticate(String username, String password) throws Exception { try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { throw new Exception("INVALID_EXCEPTION", e); } } }
排查思路&根因定位
按优先级从高到低排查:
密码未加密存储问题(最高发)
登录接口正常说明老用户的密码是经过BCryptPasswordEncoder加密后存储的,但注册逻辑中调用iUserService.saveEntity前,没有对前端传入的明文密码做加密处理,直接将明文存入数据库。
调用authenticationManager.authenticate时,DaoAuthenticationProvider会用BCrypt规则比对前端传入的明文和数据库存储的密码:数据库中存储的明文不符合BCrypt哈希格式,会直接判定为凭证无效,抛出BadCredentialsException。
当前异常捕获逻辑只抛出了自定义消息,没有打印根异常栈,加上JwtAuthenticationEntryPoint会兜底拦截所有认证相关异常返回401,就会出现无业务异常抛出、直接返回401的现象。事务未提交导致查询不到用户
如果saveEntity方法所在的类(或者Controller层的signup方法)加了@Transactional注解,且事务传播级别为默认的REQUIRED,那么在调用saveEntity后,数据库插入操作还没有提交事务,此时authenticationManager内部调用UserDetailsService查询数据库,会读不到刚插入的用户数据,判定为用户名不存在,同样会抛BadCredentialsException返回401。JWT过滤器拦截问题
检查JwtRequestFilter代码,是否没有对/flightBooking/signup路径做放行处理,不管什么请求都强行校验请求头中的JWT:注册请求本身不会携带有效JWT,过滤器检测到无效token会直接设置未认证状态,触发entryPoint返回401,根本不会走到Controller内部的authenticate逻辑——如果已经确认用户数据成功入库,这个问题概率较低。字段映射错误
检查UserMapper转换逻辑,是否将userDto.getUserPass()映射错了数据库字段,导致存入数据库的密码为空/错误,认证时密码比对失败。
解决方案
修复注册逻辑的密码加密问题
注册保存用户前,注入BCryptPasswordEncoder对明文密码加密,再存入数据库:@Autowired private BCryptPasswordEncoder passwordEncoder; @PostMapping public ResponseEntity<?> signUp(@RequestBody UserDto userDto) throws Exception { userDto.setRoleId(2L); // 加密明文密码 userDto.setUserPass(passwordEncoder.encode(userDto.getUserPass())); Long userId = this.iUserService.saveEntity(this.mapper.map(userDto)); if (userId > 0) { this.authenticate(userDto.getUsername(), userDto.getUserPass()); final UserDetails userDetails = flightBookingUserDetailsService.loadUserByUsername(userDto.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); AuthRes res = new AuthRes(token); res.setId(userId); return ResponseEntity.ok(res); } return null; }修复事务问题
不要在Controller层加事务注解,确保saveEntity方法的事务在方法返回时正常提交;如果需要在同一个事务里完成逻辑,可以在save操作后手动强制刷新ORM框架的一级缓存,或者将认证、生成token的逻辑放到事务提交完成后执行。完善异常日志
在catch块中打印异常的完整栈信息,不要只抛自定义消息,方便后续定位问题:private void authenticate(String username, String password) throws Exception { try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException e) { // 生产环境替换为日志框架打印 e.printStackTrace(); throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { e.printStackTrace(); throw new Exception("INVALID_EXCEPTION", e); } }检查JWT过滤器逻辑
在JwtRequestFilter中提前放行配置为permitAll的路径,不要对注册、登录接口做JWT校验:@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 放行注册、登录路径 String path = request.getRequestURI(); if (path.contains("/flightBooking/login") || path.contains("/flightBooking/signup")) { chain.doFilter(request, response); return; } // 原有JWT校验逻辑保持不变 }
内容的提问来源于stack exchange,提问作者hossein frkh

