基于OTP登录获取JWT Token后无法访问受保护API求助
注册后JWT访问受保护API失败的排查与解决建议
问题描述
我正在开发一款应用,客户端为注册和登录API集成了OTP验证功能,选用手机号作为用户名,未使用密码。应用后端基于Spring Boot 3构建。成功从注册API获取JWT Token后,尝试访问受保护API时遇到问题,寻求排查与解决建议。
后端核心代码
AuthController
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired AuthService authService; @PostMapping("/signup") public ResponseEntity<?> registerUser(@Valid @RequestBody CustomerCreateRequestDTO customerCreateRequestDTO) { String x = authService.registerUser(customerCreateRequestDTO); return ResponseEntity.ok(x); } }
AuthService
@Service public class AuthService { @Autowired AuthenticationManager authenticationManager; @Autowired CustomerService customerService; @Autowired JwtUtils jwtUtils; public String registerUser(@Valid @RequestBody CustomerCreateRequestDTO customerCreateRequestDTO) { if (customerService.existsByPhoneNumber(customerCreateRequestDTO.getPhoneNumber())) { throw new RuntimeException("Error: PhoneNumber is already use!"); } if (customerService.existsByEmail(customerCreateRequestDTO.getEmail())) { throw new RuntimeException("Error: Email is already in use!"); } Customer customer = customerService.createNewCustomer(customerCreateRequestDTO); String jwt = jwtUtils.generateJwtToken(customer); return jwt; } }
Customer实体
@Entity @Data @NoArgsConstructor @AllArgsConstructor @Builder @EntityListeners(AuditingEntityListener.class) public class Customer implements UserDetails { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private int id; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; private String email; @Column(nullable = false) private String phoneNumber; @Column(nullable = false) private String password; @Column(nullable = false) private String address; @Column(nullable = false) private LocalDate dateOfBirth; @Enumerated(EnumType.STRING) private Role role; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, optional = false) @JsonManagedReference private Account account; @CreatedDate @Column(nullable = false, updatable = false) private Date createdAt; @LastModifiedDate @Column(nullable = false) private Date updatedAt; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(new SimpleGrantedAuthority(this.role.name())); } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.phoneNumber; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
AuthEntryPointJwt
@Component public class AuthEntryPointJwt implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); final Map<String, Object> body = new HashMap<>(); body.put("status", HttpServletResponse.SC_UNAUTHORIZED); body.put("error", "Unauthorized"); body.put("message", authException.getMessage()); body.put("path", request.getServletPath()); final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), body); } }
AuthTokenFilter
@Component public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private CustomerDetailsService customerDetailsService; private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { Customer customer = customerDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( customer, null, customer.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); logger.info(filterChain.toString()); } } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e); } filterChain.doFilter(request, response); } private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } }
JwtUtils
@Component public class JwtUtils { private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); @Value("${app.jwtSecret}") private String jwtSecret; @Value("${app.jwtExpirationMs}") private int jwtExpirationMs; public String generateJwtToken(Customer customer) { return Jwts.builder() .setSubject(customer.getPhoneNumber()) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(key(), SignatureAlgorithm.HS256) .compact(); } private Key key() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); } public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder().setSigningKey(key()).build() .parseClaimsJws(token).getBody().getSubject(); } public boolean validateJwtToken(String authToken) { try { Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); return true; } catch (MalformedJwtException e) { logger.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { logger.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { logger.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty: {}", e.getMessage()); } return false; } }
CustomerDetailsService
@Service public class CustomerDetailsService implements UserDetailsService { @Autowired CustomerRepository customerRepository; @Override @Transactional public Customer loadUserByUsername(String username) throws UsernameNotFoundException { Customer customer = customerRepository.findByPhoneNumber(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); return customer; } }
WebSecurityConfig
@EnableWebSecurity @Configuration public class WebSecurityConfig { @Autowired private CustomerDetailsService customerDetailsService; @Autowired private AuthEntryPointJwt unauthorizedHandler; @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(customerDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**") .permitAll() .anyRequest().authenticated()) .authenticationProvider(authenticationProvider()) .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
排查步骤
- 检查请求头格式:确认访问受保护API时,请求头包含
Authorization: Bearer <你的JWT Token>,注意Bearer后有空格,Token无多余空格或换行 - 验证Token有效性:调用
JwtUtils.validateJwtToken(token)手动验证返回的Token,排查是否签名错误、已过期或格式异常 - 查看过滤器日志:检查
AuthTokenFilter中Cannot set user authentication:的具体异常栈,定位是Token解析失败、用户不存在还是其他问题 - 确认用户存储:检查注册后手机号是否正确存入数据库,
customerRepository.findByPhoneNumber能否查询到对应用户 - 检查密码字段:由于未使用密码,但
Customer实现了UserDetails,若password字段为空或未正确初始化,可能触发认证逻辑异常
解决建议
1. 修复Customer的密码处理逻辑
因为采用JWT认证无需密码校验,修改Customer类的getPassword方法:
@Override public String getPassword() { return null; // 或返回空字符串,跳过密码校验逻辑 }
同时,在创建用户时,为password字段设置合法值(避免数据库非空约束报错):
// 在CustomerService的createNewCustomer方法中注入PasswordEncoder @Autowired private PasswordEncoder passwordEncoder; public Customer createNewCustomer(CustomerCreateRequestDTO dto) { Customer customer = Customer.builder() // 其他字段赋值 .password(passwordEncoder.encode("")) // 设置加密后的空字符串 .build(); return customerRepository.save(customer); }
2. 细化日志排查
在JwtUtils.validateJwtToken中补充更详细的日志,方便快速定位Token问题;在AuthTokenFilter的catch块中打印完整异常栈:
catch (Exception e) { logger.error("Cannot set user authentication: ", e); // 打印完整栈信息 }
3. 确认客户端请求正确性
确保客户端请求受保护API时,Authorization头没有拼写错误,Token完整且未被篡改。
4. 优化Security配置
由于无需密码认证,可以移除DaoAuthenticationProvider的相关配置(因为JWT过滤器已处理认证逻辑),简化WebSecurityConfig:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**") .permitAll() .anyRequest().authenticated()) .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); }
内容的提问来源于stack exchange,提问作者Pawan
相关产品推荐
相关产品推荐

