Spring Boot集成JWT认证成功但接口返回401未授权问题
问题描述
配置了仅允许认证用户调用的接口,持有合法JWT令牌,请求时以Bearer Token格式放入Authorization请求头中。当向http://localhost:8080/api/students/all发起GET请求时,服务端返回401未授权响应:
控制台已打印日志:Authenticated user test@gmail.com[ROLE_STUDENT], setting security context,说明令牌已校验通过且安全上下文已设置,仍返回401错误。
项目核心代码
StudentController.java
@RestController @CrossOrigin(origins = "http://localhost:3000") @Slf4j @RequestMapping("/api/students") public class StudentController { @Autowired private StudentService studentService; @GetMapping("/all") public ResponseEntity<List<Student>> getAllStudents(){ log.info("test"); return new ResponseEntity<List<Student>>(studentService.getAll(), HttpStatus.OK); } }
JwtTokenUtil类代码
@Component public class JwtTokenUtil implements Serializable{ @Value("${jwt.public.key}") private String publicKey; public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(publicKey) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } public String generateToken(User user, String scope) { return doGenerateToken(user.getEmail(), scope); } private String doGenerateToken(String subject, String scope) { Claims claims = Jwts.claims().setSubject(subject); claims.put("roles",scope); return Jwts.builder() .setClaims(claims) .setIssuer("http://uni.pu.com") .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 5*60*60*1000)) .signWith(SignatureAlgorithm.HS256, publicKey) .compact(); } public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return ( username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
JWTAuthenticationFilter类代码
public class JWTAuthenticationFilter extends OncePerRequestFilter { @Autowired private MyUserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER_STRING = "Authorization"; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(HEADER_STRING); String username = null; String authToken = null; if (header != null && header.startsWith(TOKEN_PREFIX)) { authToken = header.replace(TOKEN_PREFIX, ""); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } catch (SignatureException e) { logger.error("Authentication Failed. Username or Password not valid."); } } else { logger.warn("couldn't find bearer string, will ignore the header"); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); logger.info("authenticated user " + username + userDetails.getAuthorities()+ ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(req, res); } }
WebSecurityConfig配置类代码
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true) public class WebSecurityConfig { private static final String[] WHITE_LIST_URLS = { "/", "/api/students/register", "/api/students/register/*", "/api/user/reset-password", "/api/user/save-password", "/api/user/login", "/api/user/logout", // -- Swagger UI v2 "/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/**", "/configuration/security", "/swagger-ui.html", "/webjars/**", // -- Swagger UI v3 (OpenAPI) "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui/", "/swagger-ui" }; @Autowired private JwtAuthenticationEntryPoint authenticationEntryPoint; @Value("${jwt.public.key}") private RSAPublicKey rsaPublicKey; @Value("${jwt.private.key}") private RSAPrivateKey rsaPrivateKey; @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(10); } @Bean public JWTAuthenticationFilter authenticationTokenFilterBean() throws Exception { return new JWTAuthenticationFilter(); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and(); // Set unauthorized requests exception handler http = http.exceptionHandling( (exceptions) -> exceptions.authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(new BearerTokenAccessDeniedHandler())); http = http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); http = http.formLogin().disable(); // Set permissions on endpoints http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll() // private endpoints .anyRequest().authenticated() // Set up oauth2 resource server .and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt(); return http.build(); } @Bean public JwtEncoder jwtEncoder() { JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build(); JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); return new NimbusJwtEncoder(jwks); } // Used by JwtAuthenticationProvider to decode and validate JWT tokens @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build(); } // Extract authorities from the roles claim @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; } }
MyUserDetailsService类代码
@Service @Transactional public class MyUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; public UserDetails loadUserByUsername(String email) throws UserNotFoundException { Optional<User> optionalUser = userRepository.findByEmail(email); if(!optionalUser.isPresent()) { throw new UserNotFoundException("No user found with email: " + email); } User user = optionalUser.get(); boolean enabled = user.isEnabled(); boolean accountNonExpired = true; boolean credentialsNonExpired = true; boolean accountNonLocked = true; return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(user.getRoles())); } public Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } }
故障原因
同时配置了自定义JWT认证过滤器和Spring Security默认OAuth2 Resource Server JWT认证机制,两套逻辑冲突:
- 自定义
JWTAuthenticationFilter先执行,成功解析令牌、设置SecurityContext,打印认证成功日志。 - 过滤器链向后执行时,OAuth2 Resource Server自带的
BearerTokenAuthenticationFilter会再次校验请求中的JWT。而自定义JwtTokenUtil存在签名算法不匹配问题:配置中加载的是RSA公私钥,但生成、解析JWT时使用HS256对称签名算法,直接把RSA公钥字符串当HMAC密钥使用,导致默认Resource Server的JWT解码器校验令牌失败,清空了之前设置的SecurityContext认证信息,最终返回401。
另外存在两个隐藏问题:
- 通过
@Bean注册JWTAuthenticationFilter会导致过滤器被Spring自动注册到全局过滤器链,重复执行两次。 - 令牌生成逻辑中roles字段直接传入字符串,未按数组格式存储,后续权限解析会出错。
修复方案
二选一即可,推荐使用Spring Security内置的OAuth2 Resource Server能力,减少重复代码。
方案1:移除自定义JWT过滤器,使用内置Resource Server能力(推荐)
- 修正
JwtTokenUtil签名逻辑,使用RSA256算法配合注入的RSA公私钥生成令牌,删除错误的HS256签名逻辑:
// 删除原来String类型publicKey的注入,直接注入已加载好的RSA公私钥 @Autowired private RSAPublicKey rsaPublicKey; @Autowired private RSAPrivateKey rsaPrivateKey; private Claims getAllClaimsFromToken(String token) { return Jwts.parserBuilder() .setSigningKey(rsaPublicKey) .build() .parseClaimsJws(token) .getBody(); } private String doGenerateToken(String subject, List<String> roles) { Claims claims = Jwts.claims().setSubject(subject); claims.put("roles", roles); // roles传数组/列表,不要传拼接字符串 return Jwts.builder() .setClaims(claims) .setIssuer("http://uni.pu.com") .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 5*60*60*1000)) .signWith(rsaPrivateKey, SignatureAlgorithm.RS256) // 使用RS256+RSA私钥签名 .compact(); }
- 从安全配置中删除自定义JWT过滤器相关代码:
- 删除
authenticationTokenFilterBean的Bean注册 - 删除
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)配置行
- 删除
- 内置Resource Server会自动通过已配置的
jwtDecoder和jwtAuthenticationConverter完成令牌校验、权限提取,无需手动编写解析逻辑。
方案2:保留自定义过滤器,关闭内置OAuth2 Resource Server JWT认证
如果要使用自定义JWT解析逻辑,直接删除安全配置中.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt()这段配置,避免后续过滤器重复校验令牌覆盖认证结果。同时修正JwtTokenUtil中的签名算法不匹配问题,不要将JWTAuthenticationFilter注册为单独Bean,直接在过滤器链中实例化添加即可,避免过滤器重复注册。
内容的提问来源于stack exchange,提问作者Enchantres
相关产品推荐
相关产品推荐

