JWT(Json Web Token)入门

sunjk 发布于 28 天前 79 次阅读


引言


JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全传输信息。此信息可以验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

JWT通常用于授权(最常见)、信息交换

JWT将所有数据都保存在客户端,每次请求都发回服务器

JWT的原理


JWT原理是,服务器认证通过后,生成一个类似以下内容的JSON对象,将其发给用户,并且用户与服务端通信都要再发回这个JSON对象。

{
    "name": "sunjk",
    "character": "admin",
    "experiation_time": "2026年1月1日0点0分"
}

JWT的组成


JWT的组成如下图所示,包含三个部分(用.隔开)

  • Header:头部
  • Payload:负载
  • Signature:签名

Header

Header部分是一个JSON对象,描述JWT的元数据,通常如下所示。之后,使用Base64URL算法,将其转为字符串

{
    "alg": "HS256", // 签名的算法
    "typ": "JWT"  // 令牌的类型
}

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。Payload也使用Base64URL算法转为字符串。

  • 注册声明:预定义的声明
    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号
  • 公共声明:可随意定义,但为了避免冲突,应该在IANA JSON Web Token Registry中定义
  • 私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature

Signature是对前两个部分的签名,防止数据篡改

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

JWT的使用方式

客户端收到JWT后,将其存放在cookies中或本地,在之后与服务器的通信中,都要携带JWT

在Java中使用JWT


示例


以下示例中使用RSA非对称签名算法,而非HMACSHA256;使用双令牌(accessToken和refreshToken)

认证相关配置属性 AuthProperties.java

@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
    private final Jwt jwt = new Jwt();

    @Data
    public static class Jwt {
        // JWT签名者标识
        private String issuer = "zhiguang";
        // 访问令牌有效期
        private Duration accessTokenTtl = Duration.ofMinutes(15);
        // 刷新令牌有效期
        private Duration refreshTokenTtl = Duration.ofDays(7);
        // JWT密钥标识,用于下游校验与轮换
        private String keyId = "zhiguang-key"
        // RSA 私钥 PEM 资源
        private Resource privateKey;
        // RSA 公钥 PEM 资源
        private Resource publicKey;
    }
}

PEM 密钥读取工具 PemUtils.java

因与JWT无太大关联,故在此省略

私钥 private.pem

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDONBk+TKRZ1ok1
mOksrWXKCiNrC+rk/uCpaguItFkM6PBjrfBEoHsY99UbAIh/9kxjotjgLqqKqou9
EG/Yg0Aq07RAAH3NRPb684vU8evV0HKtWON1BiPCCPq/HWFJvUqm08Q2s4qMUso7
rvNoyJ1ZAJ4mJO6yzhQ/lXB3XbLZvJOjoevaDn2zGKI+lneyhACZlbGQsP4NybQE
T5YRbJScWg+Svd+KweqB2JzR5BKlVJOOOgwoptzV79//vbd2USwwyG28umoATxtj
+C5NqiaLA8r3bPFPNqOsVSY11Z5Yzetp1mShImBPYpXtjjVTm7sPiloyZozP54I2
+0Z8A5E1AgMBAAECggEASdHw75K0i8z4Fg4F/A6fkDMM9vevQgfVGhFyy5/0+WAz
HxxYFdl1+DxROHd6ZoUgV9Mm3SsXtsjTef32bIagZDSiJP+ICgKUgMbL2pI5IbSc
OV7HH0/xYTehvOWhvKpdnVfWuNaR1SaWa8N4NJ0NZ52HAJTcN3r7WMABIVqT+sqZ
baO1R+OW/PmSRqPVuX1Yzaku76GIaqsYAfx3ODwAr1WDOhKgXZP7uaqH94IM43I1
WM3d12ombZvu3lIRSIgpF6VNXr0zUPaNGh1jx49oVwp/sMFVZ0tx31T50//Tzj8R
eKJLF7xZXtxiTnsamIvuxM0hI7OOOlK+aYJskzDSiQKBgQDqjepkgJ/M7H8LmbFX
BFj4w35wg+/sClO7rPx1Jriz/Y1r3+KHtkbTsDXTeSFpJP5fSMssbMQkbRDX2Qv/
E8Xnci1TOMn9nVGhYIdlwLrNgHPRfGMUGPEZn5BZQpq1toybIy0MYr5TGhlDBDHT
x5oM0d7sEAs/dGGDPPO6CKNNWwKBgQDhDpciUMJkZAsYdshnvemJvBmxE+gna1HN
g3dtl/A05/fXiBurzQOCYsvndhyLdQri0YrSUifVgG+Dd6rZuSRQYMlbMmdrxhwH
BfdzipvbHtONB+RIs/AzUZZ1bmDcN0sdbsuyQuRxeCzVLr24NTwFCvofdHebVKkZ
tN1i+GQQrwKBgQC8ixGUclSCPMUvRP9p7GO8rCESPux4eCCw5zAU1+h3PuUUhuao
otwyRB4wfNlflM4pN/GiJ1hP06BLcfyTsYhPMNfAYxDznidFKrwMDi3bDVuMVIms
WRRNvZciOUj1suU5u7/1idZmJL+TwGZxT6zEi1u/tHHmMx5DiZZ9v33NqwKBgDQj
nDHwAckeQE/PnQgjwEoPkOgsU3lmbJIvJej27UhkgU+mkbqQiYKYfuLFw0EvwXDg
2Md3DxFwauVLQQKl2NriQWPmXn1ibTXXVQeokgkGsD4ZGPMa9Mf+29ui0b/S840D
ER/gTWpYTBKrfHqDor8MVvwYkGklCPQSqcN3Bj+zAoGACrNC/t+FI1kaXQ8XOErN
zSFwUJav/FUehz0y3PHXeruA+f0wtSIUnMcMPsKLkJeLJs5w4aXKdbn8HOQULdFq
jbQmjVaGE61koQUywFWDIh+g3R8ttOE3RuCLFlfg6YmKQuYf9EdjZZ7t/z/aY483
FmLYciOPHqUiy7/JQe95JGU=
-----END PRIVATE KEY-----

公钥 public.pem

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjQZPkykWdaJNZjpLK1l
ygojawvq5P7gqWoLiLRZDOjwY63wRKB7GPfVGwCIf/ZMY6LY4C6qiqqLvRBv2INA
KtO0QAB9zUT2+vOL1PHr1dByrVjjdQYjwgj6vx1hSb1KptPENrOKjFLKO67zaMid
WQCeJiTuss4UP5Vwd12y2byTo6Hr2g59sxiiPpZ3soQAmZWxkLD+Dcm0BE+WEWyU
nFoPkr3fisHqgdic0eQSpVSTjjoMKKbc1e/f/723dlEsMMhtvLpqAE8bY/guTaom
iwPK92zxTzajrFUmNdWeWM3radZkoSJgT2KV7Y41U5u7D4paMmaMz+eCNvtGfAOR
NQIDAQAB
-----END PUBLIC KEY-----

访问令牌与刷新令牌的组合 TokenPair.java

public record TokenPair {
    // 访问令牌
    String accessToken,
    // 访问令牌过期时间
    Instant accessTokenExpiresAt,
    // 刷新令牌
    String refreshToken,
    // 刷新令牌过期时间
    Instant refreshTokenExpiresAt,
    // 刷新令牌ID(jti,用于白名单存储和撤销)
    String refreshTokenId
}

刷新令牌白名单存储接口 RefreshTokenStore.java

/**
 * 刷新令牌白名单存储接口。
 * <p>
 * 负责管理 Refresh Token 的有效性:存储、校验、撤销单个令牌与撤销用户全部令牌。
 * 实现可使用 Redis、数据库或其它持久化方案。
 */
public interface RefreshTokenStore {

    /**
     * 存储刷新令牌白名单记录
     * @param userId 用户ID
     * @param tokenId 刷新令牌ID
     * @param ttl 生存时间
     */
    void storeToken(long userId, String tokenId, Duration ttl);
  
    /**
     * 校验刷新令牌是否有效(是否在白名单内且未过期)
     * @param userId 用户ID
     * @param tokenId 刷新令牌ID
     * @return 是否有效
     */
    boolean isTokenVaild(long userId, String tokenId);
  
    /**
     * 撤销指定刷新令牌(从白名单中移除)
     * @param userId 用户ID
     * @param tokenId 刷新令牌ID
     */
    void revokeToken(long userId, String tokenId);
  
    /**
     * 撤销用户的所有刷新令牌
     * @param userId 用户ID
     */
    void revokeAll(long userId);

基于 Redis 的刷新令牌白名单存储 RedisRefreshTokenStore.java

@Comonent
public class RedisRefreshTokenStore implements RefreshTokenStore {

    // 操作Redis的工具对象
    private final StringRedisTemplate redisTemplate;


    public RedisRefreshTokenStore(StringRedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    &nbsp;
    &nbsp;/**
&nbsp; &nbsp; &nbsp;* 将刷新令牌写入白名单,设置过期时间。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param userId &nbsp;用户 ID。
&nbsp; &nbsp; &nbsp;* @param tokenId 刷新令牌 ID。
&nbsp; &nbsp; &nbsp;* @param ttl &nbsp; &nbsp; 生存时间(Redis TTL)。
&nbsp; &nbsp; &nbsp;*/
    @Override
    public void storeToken(long userId, String tokenId, Duration ttl) {
        String key = key(userId, tokenId);
        // 存入键名、值(1代表有效)、过期时间
        redisTemplate.opsForValue().set(key, "1", ttl);
    }

    /**
&nbsp; &nbsp; &nbsp;* 判断刷新令牌是否仍有效。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param userId &nbsp;用户 ID。
&nbsp; &nbsp; &nbsp;* @param tokenId 刷新令牌 ID。
&nbsp; &nbsp; &nbsp;* @return 是否有效(键存在且值为 "1")。
&nbsp; &nbsp; &nbsp;*/
    @Override
    public boolean isTokenValid(long userId, String tokenId) {
        String key = key(userId, tokenId);
        return Objects.equals("1", redisTemplate.opsForValue().get(key));
    }

    /**
&nbsp; &nbsp; &nbsp;* 撤销单个刷新令牌。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param userId &nbsp;用户 ID。
&nbsp; &nbsp; &nbsp;* @param tokenId 刷新令牌 ID。
&nbsp; &nbsp; &nbsp;*/
    @Override
    public void revokeToken(long userId, String tokenId) {
        redisTemplate.delete(key(userId, tokenId));
    }

    &nbsp;/**
&nbsp; &nbsp; &nbsp;* 撤销该用户全部刷新令牌。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param userId 用户 ID。
&nbsp; &nbsp; &nbsp;*/
    @Override
    public void revokeAll(long userId) {
        String pattern = "auth:rt:%d:*".formatted(userId);
        var keys = redisTemplate.keys(pattern);
        if(!keys.isEmpty()){
            redisTemplate.delete(keys);
        }
    }

    /**
&nbsp; &nbsp; &nbsp;* 生成白名单键名。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param userId &nbsp;用户 ID。
&nbsp; &nbsp; &nbsp;* @param tokenId 刷新令牌 ID。
&nbsp; &nbsp; &nbsp;* @return Redis 键名。
&nbsp; &nbsp; &nbsp;*/
    private static String key(long userId, String tokenId) {
        return "auth:rt:%d:%s".formatted(userId, tokenId);
    }
}

JWT 令牌服务 JwtService.java

/**
&nbsp;* JWT 令牌服务。
&nbsp;* <p>
&nbsp;* 功能:签发 Access/Refresh Token(RS256),解码 JWT,提取用户 ID、令牌类型与令牌 ID。
&nbsp;* 声明:
&nbsp;* - `token_type`:标识 access 或 refresh;
&nbsp;* - `uid`:用户 ID;
&nbsp;* - `jti`:令牌 ID(用作 Refresh Token 的白名单键)。
&nbsp;* 过期时间:来自 `AuthProperties.jwt.accessTokenTtl` 与 `refreshTokenTtl`。
&nbsp;*/
@Service
@RequiredArgsConstructor
public class JwtService {

&nbsp; &nbsp; private static final String CLAIM_TOKEN_TYPE = "token_type";
&nbsp; &nbsp; private static final String CLAIM_USER_ID = "uid";

&nbsp; &nbsp; private final JwtEncoder jwtEncoder;
&nbsp; &nbsp; private final JwtDecoder jwtDecoder;
&nbsp; &nbsp; private final AuthProperties properties;
&nbsp; &nbsp; private final Clock clock = Clock.systemUTC();

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 为指定用户签发一对 Access/Refresh Token。
&nbsp; &nbsp; &nbsp;* <p>
&nbsp; &nbsp; &nbsp;* 令牌类型通过 `token_type` 声明区分;Refresh Token 的 `jti` 用于白名单存储与撤销。
&nbsp; &nbsp; &nbsp;* 过期时间取自配置 `AuthProperties.jwt`。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param user 用户实体。
&nbsp; &nbsp; &nbsp;* @return 令牌对与对应过期时间及刷新令牌 ID。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; public TokenPair issueTokenPair(User user) {
&nbsp; &nbsp; &nbsp; &nbsp; String refreshTokenId = UUID.randomUUID().toString();
&nbsp; &nbsp; &nbsp; &nbsp; Instant issuedAt = Instant.now(clock);
&nbsp; &nbsp; &nbsp; &nbsp; Instant accessExpiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl());
&nbsp; &nbsp; &nbsp; &nbsp; Instant refreshExpiresAt = issuedAt.plus(properties.getJwt().getRefreshTokenTtl());
&nbsp; &nbsp; &nbsp; &nbsp; String accessToken = encodeToken(user, issuedAt, accessExpiresAt, "access", UUID.randomUUID().toString());
&nbsp; &nbsp; &nbsp; &nbsp; String refreshToken = encodeRefreshToken(user, issuedAt, refreshExpiresAt, refreshTokenId);
&nbsp; &nbsp; &nbsp; &nbsp; return new TokenPair(accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, refreshTokenId);
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 解码 JWT 字符串为 {@link Jwt}。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param token JWT 字符串。
&nbsp; &nbsp; &nbsp;* @return 解析后的 JWT 对象。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; public Jwt decode(String token) {
&nbsp; &nbsp; &nbsp; &nbsp; return jwtDecoder.decode(token);
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 编码访问令牌。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param user &nbsp; &nbsp; &nbsp;用户实体,作为 subject 与自定义声明来源。
&nbsp; &nbsp; &nbsp;* @param issuedAt &nbsp;签发时间。
&nbsp; &nbsp; &nbsp;* @param expiresAt 过期时间。
&nbsp; &nbsp; &nbsp;* @param tokenType 令牌类型("access")。
&nbsp; &nbsp; &nbsp;* @param tokenId &nbsp; 令牌 ID(jti)。
&nbsp; &nbsp; &nbsp;* @return 编码后的 JWT 字符串。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; private String encodeToken(User user, Instant issuedAt, Instant expiresAt, String tokenType, String tokenId) {
&nbsp; &nbsp; &nbsp; &nbsp; JwtClaimsSet claims = JwtClaimsSet.builder()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .issuer(properties.getJwt().getIssuer())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .issuedAt(issuedAt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .expiresAt(expiresAt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .subject(String.valueOf(user.getId()))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .id(tokenId)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .claim(CLAIM_TOKEN_TYPE, tokenType)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .claim(CLAIM_USER_ID, user.getId())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .claim("nickname", user.getNickname())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .build();
&nbsp; &nbsp; &nbsp; &nbsp; return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 编码刷新令牌。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param user &nbsp; &nbsp; &nbsp;用户实体。
&nbsp; &nbsp; &nbsp;* @param issuedAt &nbsp;签发时间。
&nbsp; &nbsp; &nbsp;* @param expiresAt 过期时间。
&nbsp; &nbsp; &nbsp;* @param tokenId &nbsp; 刷新令牌 ID(jti)。
&nbsp; &nbsp; &nbsp;* @return 编码后的刷新令牌字符串。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; private String encodeRefreshToken(User user, Instant issuedAt, Instant expiresAt, String tokenId) {
&nbsp; &nbsp; &nbsp; &nbsp; JwtClaimsSet claims = JwtClaimsSet.builder()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .issuer(properties.getJwt().getIssuer())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .issuedAt(issuedAt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .expiresAt(expiresAt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .subject(String.valueOf(user.getId()))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .id(tokenId)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .claim(CLAIM_TOKEN_TYPE, "refresh")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .claim(CLAIM_USER_ID, user.getId())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .build();
&nbsp; &nbsp; &nbsp; &nbsp; return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 从 JWT 中提取用户 ID。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param jwt 已解析的 JWT。
&nbsp; &nbsp; &nbsp;* @return 用户 ID(long)。
&nbsp; &nbsp; &nbsp;* @throws IllegalArgumentException 当声明类型不合法时抛出。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; public long extractUserId(Jwt jwt) {
&nbsp; &nbsp; &nbsp; &nbsp; Object claim = jwt.getClaims().get(CLAIM_USER_ID);
&nbsp; &nbsp; &nbsp; &nbsp; if (claim instanceof Number number) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return number.longValue();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; if (claim instanceof String text) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return Long.parseLong(text);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; throw new IllegalArgumentException("Invalid user id in token");
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 提取令牌类型声明。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param jwt 已解析的 JWT。
&nbsp; &nbsp; &nbsp;* @return 令牌类型字符串(例如:"access" 或 "refresh")。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; public String extractTokenType(Jwt jwt) {
&nbsp; &nbsp; &nbsp; &nbsp; Object claim = jwt.getClaims().get(CLAIM_TOKEN_TYPE);
&nbsp; &nbsp; &nbsp; &nbsp; return claim != null ? claim.toString() : "";
&nbsp; &nbsp; }

&nbsp; &nbsp; /**
&nbsp; &nbsp; &nbsp;* 提取令牌 ID(jti)。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param jwt 已解析的 JWT。
&nbsp; &nbsp; &nbsp;* @return 令牌 ID。
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; public String extractTokenId(Jwt jwt) {
&nbsp; &nbsp; &nbsp; &nbsp; return jwt.getId();
&nbsp; &nbsp; }
}

References


JSON Web Token 入门教程
JWT中文文档网

此作者没有提供个人介绍。
最后更新于 2026-01-11