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

重启REST应用后同一JWT验证失败:签名不匹配问题排查

问题描述

开发REST服务时,JWT生成和即时验证正常,但重启应用后,使用同一token验证会抛出错误:

"JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted"

测试流程:

  • 生成token后立即测试,验证通过
  • 重建/重启项目后,使用同一token测试,验证失败

用于生成和验证token的TokenUtils类代码:

package com.keroles.jobify.Sec.Token.Util;

import com.keroles.jobify.Sec.Token.Model.TokenModel;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

@Component
@Slf4j
@Getter
public class TokenUtils {
    private final long ACCESS_TOKEN_VALIDITY=432000L;//7days
    private final long REFRESH_TOKEN_VALIDITY=604800L;//7days
    @Getter(AccessLevel.NONE)
    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);


    public String generateToken(Authentication authentication ,long validityTime){
 log.error("{}",key.getEncoded());
 log.error(key.getEncoded().toString());
 return Jwts
                .builder()
                .setClaims(prepareClaims(authentication))
                .setSubject(authentication.getName())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()
                        + validityTime * 1000))
                .signWith(key)
                .compact();
    }


    public TokenModel getTokenModel(String token){
        Claims claims= getAllClaimsFromToken(token);
        return TokenModel
                .builder()
                .username(claims.getSubject())
                .roles((List<String>)claims.get("roles"))
                .createdAt(new Date( (Long) claims.get("created")))
                .expirationDate(claims.getExpiration())
                .build();
    }

    public boolean validateToken(TokenModel tokenModel, UserDetails userDetails){
        return (userDetails!=null
                && tokenModel.getUsername().equals(userDetails.getUsername())
                && ! isTokenExpired(tokenModel.getExpirationDate()));
    }

    private boolean isTokenExpired(Date expirationDate) {
        return expirationDate.before(new Date());
    }
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
    private Map<String,Object> prepareClaims(Authentication authentication){
        List<String>authority=authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        Map<String, Object> claims = new HashMap<>();
        claims.put("created",new Date());
        claims.put("roles",authority);
        return claims;
    }
}
问题原因

你的推测完全正确:当前代码中使用Keys.secretKeyFor(SignatureAlgorithm.HS256)生成签名密钥,这是一个每次应用启动时都会随机生成的临时密钥。重启后密钥变更,旧token是用之前的密钥签名的,新的密钥无法验证通过,就会出现签名不匹配的错误。

解决方案

要解决这个问题,需要使用固定的签名密钥,而不是每次启动生成新密钥。可以通过以下两种方式实现:

方式1:从配置文件读取固定密钥(推荐生产环境使用)

  1. 在application.properties或application.yml中添加配置:
# application.properties
jwt.secret=your-fixed-secret-key-here-min-32-chars-for-HS256

注意:HS256算法要求密钥长度至少为256位(即32个字符以上),建议使用随机生成的长字符串作为密钥,比如通过openssl rand -hex 32命令生成。

  1. 修改TokenUtils类,从配置中读取密钥并生成固定的Key:
package com.keroles.jobify.Sec.Token.Util;

import com.keroles.jobify.Sec.Token.Model.TokenModel;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

@Component
@Slf4j
@Getter
public class TokenUtils {
    private final long ACCESS_TOKEN_VALIDITY=432000L;//7days
    private final long REFRESH_TOKEN_VALIDITY=604800L;//7days
    @Getter(AccessLevel.NONE)
    private final Key key;

    // 从配置文件注入固定密钥
    public TokenUtils(@Value("${jwt.secret}") String secret) {
        // 使用Keys.hmacShaKeyFor将字符串转换为符合HS256要求的Key
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
    }


    public String generateToken(Authentication authentication ,long validityTime){
        log.error("{}",key.getEncoded());
        log.error(key.getEncoded().toString());
        return Jwts
                .builder()
                .setClaims(prepareClaims(authentication))
                .setSubject(authentication.getName())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()
                        + validityTime * 1000))
                .signWith(key)
                .compact();
    }


    public TokenModel getTokenModel(String token){
        Claims claims= getAllClaimsFromToken(token);
        return TokenModel
                .builder()
                .username(claims.getSubject())
                .roles((List<String>)claims.get("roles"))
                .createdAt(new Date( (Long) claims.get("created")))
                .expirationDate(claims.getExpiration())
                .build();
    }

    public boolean validateToken(TokenModel tokenModel, UserDetails userDetails){
        return (userDetails!=null
                && tokenModel.getUsername().equals(userDetails.getUsername())
                && ! isTokenExpired(tokenModel.getExpirationDate()));
    }

    private boolean isTokenExpired(Date expirationDate) {
        return expirationDate.before(new Date());
    }
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
    private Map<String,Object> prepareClaims(Authentication authentication){
        List<String> authority=authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        Map<String, Object> claims = new HashMap<>();
        claims.put("created",new Date());
        claims.put("roles",authority);
        return claims;
    }
}

方式2:硬编码固定密钥(仅测试环境临时使用)

如果只是测试环境临时使用,可以直接硬编码一个固定密钥,但生产环境绝对不能这么做,因为密钥泄露会导致JWT被伪造:

// 修改TokenUtils中的key初始化代码
@Getter(AccessLevel.NONE)
private final Key key = Keys.hmacShaKeyFor("your-fixed-32-char-or-longer-secret-key-here".getBytes());

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 13:45:30