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

如何测试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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 23:29:53