如何测试JWT的generateToken()?解决iat/exp时间差异导致的测试失败
问题
我想测试用于生成和处理JWT令牌的JwtService,验证令牌生成是否正确。虽然不确定是否必须测试该方法(因为并非所有实现都是自定义的),但仍想确认令牌生成的正确性。然而,当对比生成的令牌与预先生成的参考令牌时,由于iat(签发时间)和exp(过期时间)为动态时间,二者存在差异,导致测试始终失败。请问如何绕过时间差异完成有效测试?
JwtService代码:
package de.gabriel.vertretungsplan.security.service; import de.gabriel.vertretungsplan.models.UserDetailsImpl.UserDetailsImpl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Service; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Service public class JwtService { /* Generated with: https://www.allkeysgenerator.com/; 256-bit key */ private static final String SECRET_KEY = "secret_key"; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } public String generateToken(UserDetailsImpl userDetails) { return generateToken(new HashMap<>(), userDetails); } public String generateToken( Map<String, Object> extraClaims, UserDetailsImpl userDetails ) { return Jwts.builder() .setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24 * 7))) // 7 days .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public boolean isTokenValid(String token, UserDetailsImpl userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private Claims extractAllClaims(String token) { return Jwts .parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); } private Key getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); return Keys.hmacShaKeyFor(keyBytes); } }
测试代码(原失败版本):
@Test public void generateToken_shouldGenerateToken() { Verwaltung verwaltung = new Verwaltung( "user", "user@gmail.com", "user", Rolle.getPrefixedRolle(Rolle.USER) ); String generatedToken = jwtService.generateToken(new UserDetailsImpl(verwaltung)); assertEquals(token, generatedToken); }
注:预期token是通过jwt.io使用相同密钥生成的参考令牌。
解决方案
针对动态时间导致的令牌对比失败,有三种可行的解决思路:
方法1:抽离时间逻辑,测试时固定时间(推荐)
把System.currentTimeMillis()的获取逻辑封装成可重写的方法,测试时替换为固定时间戳,这样生成的令牌就能和预先生成的参考值完全匹配。
修改JwtService:
@Service public class JwtService { // ... 原有代码保留 // 新增:抽离时间获取逻辑,方便测试替换 protected long getCurrentTimeMillis() { return System.currentTimeMillis(); } // 修改generateToken方法,使用抽离的时间获取逻辑 public String generateToken( Map<String, Object> extraClaims, UserDetailsImpl userDetails ) { long now = getCurrentTimeMillis(); return Jwts.builder() .setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(now)) .setExpiration(new Date(now + (1000 * 60 * 60 * 24 * 7))) // 7 days .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } // ... 原有代码保留 }
编写测试用例:
@Test public void generateToken_shouldMatchExpectedToken() { // 固定测试用的时间戳(需和jwt.io生成参考令牌时用的时间一致) long fixedTime = 1620000000000L; // 创建JwtService的测试子类,替换时间获取逻辑 JwtService testJwtService = new JwtService() { @Override protected long getCurrentTimeMillis() { return fixedTime; } }; Verwaltung verwaltung = new Verwaltung( "user", "user@gmail.com", "user", Rolle.getPrefixedRolle(Rolle.USER) ); String generatedToken = testJwtService.generateToken(new UserDetailsImpl(verwaltung)); // 替换为用fixedTime在jwt.io生成的参考令牌 String expectedToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjIwMDAwMDAwLCJleHAiOjE2MjYwNDgwMDB9.SW4E6nXzQ8e7UcQZ8aZ8aZ8aZ8aZ8aZ8aZ8aZ8"; assertEquals(expectedToken, generatedToken); }
方法2:解析令牌验证核心Claims,而非全字符串对比
不直接对比整个令牌字符串,而是解析生成的令牌,验证其中的关键字段是否符合预期,这种方法无需固定时间,更关注令牌的有效性而非字符串完全一致。
测试用例:
@Test public void generateToken_shouldContainValidClaims() { Verwaltung verwaltung = new Verwaltung( "user", "user@gmail.com", "user", Rolle.getPrefixedRolle(Rolle.USER) ); String generatedToken = jwtService.generateToken(new UserDetailsImpl(verwaltung)); // 解析令牌获取Claims(extractAllClaims会自动验证签名,解析成功则签名有效) Claims claims = jwtService.extractAllClaims(generatedToken); // 验证核心字段 assertEquals("user", claims.getSubject()); // 用户名正确 // 验证过期时间与签发时间的间隔为7天(允许1秒内的误差) long timeDiff = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); assertTrue(timeDiff >= 1000 * 60 * 60 * 24 * 7 && timeDiff <= 1000 * 60 * 60 * 24 * 7 + 1000); // 验证签发时间为当前时间附近(允许少量误差) assertTrue(Math.abs(claims.getIssuedAt().getTime() - System.currentTimeMillis()) <= 1000); }
方法3:用Mockito静态mock固定系统时间
如果不想修改原JwtService代码,可以用Mockito的静态mock功能直接替换System.currentTimeMillis()的返回值(需要依赖mockito-inline)。
测试用例:
@Test public void generateToken_withMockedSystemTime() { long fixedTime = 1620000000000L; // 静态mock System.currentTimeMillis() try (MockedStatic<System> mockedSystem = Mockito.mockStatic(System.class)) { mockedSystem.when(System::currentTimeMillis).thenReturn(fixedTime); Verwaltung verwaltung = new Verwaltung( "user", "user@gmail.com", "user", Rolle.getPrefixedRolle(Rolle.USER) ); String generatedToken = jwtService.generateToken(new UserDetailsImpl(verwaltung)); // 替换为用fixedTime生成的参考令牌 String expectedToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjIwMDAwMDAwLCJleHAiOjE2MjYwNDgwMDB9.SW4E6nXzQ8e7UcQZ8aZ8aZ8aZ8aZ8aZ8aZ8"; assertEquals(expectedToken, generatedToken); } }
内容的提问来源于stack exchange,提问作者Gabriel
相关产品推荐
相关产品推荐

