签名有效的JWT为何触发Spring Boot+React的401未授权错误?
问题描述
基于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
相关产品推荐
相关产品推荐

