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

Spring Boot集成JWT认证成功但接口返回401未授权问题

问题描述

配置了仅允许认证用户调用的接口,持有合法JWT令牌,请求时以Bearer Token格式放入Authorization请求头中。当向http://localhost:8080/api/students/all发起GET请求时,服务端返回401未授权响应:
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认证机制,两套逻辑冲突:

  1. 自定义JWTAuthenticationFilter先执行,成功解析令牌、设置SecurityContext,打印认证成功日志。
  2. 过滤器链向后执行时,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能力(推荐)

  1. 修正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();
}
  1. 从安全配置中删除自定义JWT过滤器相关代码:
    • 删除authenticationTokenFilterBean的Bean注册
    • 删除http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)配置行
  2. 内置Resource Server会自动通过已配置的jwtDecoder和jwtAuthenticationConverter完成令牌校验、权限提取,无需手动编写解析逻辑。

方案2:保留自定义过滤器,关闭内置OAuth2 Resource Server JWT认证

如果要使用自定义JWT解析逻辑,直接删除安全配置中.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt()这段配置,避免后续过滤器重复校验令牌覆盖认证结果。同时修正JwtTokenUtil中的签名算法不匹配问题,不要将JWTAuthenticationFilter注册为单独Bean,直接在过滤器链中实例化添加即可,避免过滤器重复注册。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 21:06:22