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

签名有效的JWT为何触发Spring Boot+React的401未授权错误?

JWT签名验证有效但访问受保护端点返回401 Unauthorized问题排查

问题描述

基于Spring Boot和React实现用户认证与授权功能,访问受保护端点时,签名验证有效的JWT令牌却返回401 Unauthorized错误。

相关代码

程序入口

@SpringBootApplication//(exclude = { SecurityAutoConfiguration.class })
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = 1800)
public class Backend3Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(Backend3Application.class, args);
    }

}

JWT服务类

package com.example.demo.service;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
import java.util.function.Function;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.xml.bind.DatatypeConverter;

@Service
public class JwTTokenProviderService {
    
    @Value("${jwt.secret}")
    private String JWT_SECRET_KEY;

    // JWT_SECRET_KEY在application.properties中配置
    
    public String generateToken(String username, String role) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        Key signingKey = new SecretKeySpec(DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY), signatureAlgorithm.getJcaName());
        long jwtExpirationInMs = 36000;
        
        System.out.println("Username at generating token is " + username);
        System.out.println("Role at generating token is " + role);
        
        return Jwts.builder()
                .setId(UUID.randomUUID().toString())
                .setSubject("Me")
                .claim("hasRole", role)
                .claim("username",  username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationInMs))
                .signWith(signingKey, signatureAlgorithm)
                .compact();
    }
    
    public String getAllClaimsFromToken(String token) {
        System.out.println("Token at getAllClaimsFromToken is " + token);
        try {
            byte[] arrSecret = DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY);
            Key signingKey = new SecretKeySpec(arrSecret, SignatureAlgorithm.HS256.getJcaName());
            Jws<Claims> jwsClaims = Jwts.parser()
                    .setSigningKey(signingKey)
                    .parseClaimsJws(token);
            Claims claims = jwsClaims.getBody();
            return claims.get("hasRole", String.class);
        } catch (Exception e) {
            System.out.println("Could not get all claims from passed token");
            System.out.println("Exception is " + e);
            return null;
        }
    }

    public boolean validateToken(String token) {
        try {
            byte[] arrSecret = DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY);
            Key signingKey = new SecretKeySpec(arrSecret, SignatureAlgorithm.HS256.getJcaName());
            Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            System.out.println("Error is " + e);
        }
        return false;
    }
    
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, claims -> claims.get("username", String.class));
    }
    
    public String getRoleFromToken(String token) {
        return getClaimFromToken(token, claims -> claims.get("hasRole", String.class));
    }
    
    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = Jwts.parser()
                .setSigningKey(new SecretKeySpec(DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY), SignatureAlgorithm.HS256.getJcaName()))
                .parseClaimsJws(token)
                .getBody();
        return claimsResolver.apply(claims);
    }
}

前端请求方法

getInfoForUser(username) {
        try {
            console.log("Username at Axios is, ", username)
            console.log("User token at getInfoForUser is: ", localStorage.getItem('userToken'))
            return axios.post("http://myserver:8080/myapp/login/get-info-for-user", {
                params: { username: username },
                headers: {
                    'Content-Type':'application/json; charset=UTF-8',
                    'Authorization': `${localStorage.getItem("userToken")}`,
                    'Accept': 'application/json'
                }
            })
        } catch (error) {
            console.log("Error is, ", error)
        }
    }

控制器受保护方法

@PreAuthorize("hasRole('ROLE_USER')")
    @PostMapping("/get-info-for-user")
    public Object[] getInfoForUser(@Param("username") String username) {
        return userService.getInfoForUser(username);
    }

JWT响应模型

package com.example.demo.model;

public class JwtResponse {
    private String token;
    private String type = "Bearer";
    private String username;
    private String hasRole;
    
    public String getToken() { return token; }
    public void setToken(String token) { this.token = token; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String gethasRole() { return hasRole; }
    public void sethasRole(String hasRole) { this.hasRole = hasRole; }
    
    public JwtResponse(String token, String username, String hasRole) {
        super();
        this.token = token;
        this.username = username;
        this.hasRole = hasRole;
    }
}

安全配置类

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .cors(cors -> cors.disable())
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/myapp/**", "/public/**", "/login/**").permitAll()
                        .anyRequest().authenticated())
                .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .httpBasic(Customizer.withDefaults())
                .build();
    }
}

JwtTokenFilter类

package com.example.demo;

import java.io.IOException;
import java.util.Collections;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.example.demo.service.JwTTokenProviderService;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwTTokenProviderService jwtTokenService;

    public JwtTokenFilter(JwTTokenProviderService jwtTokenService) {
        this.jwtTokenService = jwtTokenService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        System.out.println("JwtFilter hit for request: " + request.getRequestURI());
        String token = resolveToken(request);
        if (token != null && jwtTokenService.validateToken(token)) {
            Authentication auth = getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }
    
    private Authentication getAuthentication(String token) {
        System.out.println("Token at authentication for secured endpoint is: " + token);
        String username = jwtTokenService.getUsernameFromToken(token);
        String role = jwtTokenService.getRoleFromToken(token);
        System.out.println("Retrieved data for token is: " + username + " " + role);
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role);
        return new UsernamePasswordAuthenticationToken(username, "", Collections.singletonList(authority));
    }
    
    private String resolveToken(HttpServletRequest req) {
        String bearerToken = req.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

已做排查

  • 前后端均使用POST方法(尝试过GET,结果一致);
  • JWT密钥已正确配置;
  • 数据库角色已添加ROLE_前缀,适配@PreAuthorize要求;
  • 令牌签名验证有效;
  • 授权头格式正确,仅包含令牌;
  • 已启用Pre/Post注解。

更新记录

  • 新增JwtTokenFilter并配置到安全链;
  • 调整令牌结构,将username和role设为claims;
  • 修复令牌生成与验证的密钥逻辑;
  • 注释掉SecurityAutoConfiguration排除配置,使过滤器生效;

目前过滤器能接收请求,但无法从请求中获取有效授权信息,请求定位问题根源。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 03:50:00