重启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:从配置文件读取固定密钥(推荐生产环境使用)
- 在
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命令生成。
- 修改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
相关产品推荐
相关产品推荐

