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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 03:27:01