JWT

发布于 2026-08-03 09:56 更新于 2026-08-03 09:56 1863 字 10 min read ... 访问量

JWT(JSON Web Token)是一种用于在系统间传输身份与授权信息的紧凑、URL安全的令牌格式,支持签名和加密,常用于身份认证、权限控制和系统间通信。其结构由头、载荷和签名三部分组成,通过Base64URL编码传输,签名确保数据完整性与来源可信,但载荷内容未加密,存在信息泄露风险。JWT具备跨语言、分布式验证等优势,但存在令牌难撤销、权限过期等局限,使用时需严格校验签名、有效期和算法,并结合HTTPS、安全存储等措施防范安全风险。

JWT

JWT 概述

JWT(JSON Web Token)是一种紧凑、URL 安全的声明传输格式,常用于在不同系统之间传递身份与授权相关信息。

JWT 本身是一种令牌格式,并不等同于登录协议或单点登录方案。它可以作为认证、授权和单点登录流程中的一种令牌载体。

JWT 是否保密取决于具体封装方式。常见的三段式 JWT 属于 JWS,仅提供签名或消息认证码保护,载荷内容仍可被读取;只有使用 JWE 时,载荷才会被加密。

JWT 的常见应用场景

  1. 身份认证

    用户登录成功后,服务端签发 JWT。客户端在后续请求中携带该令牌,服务端通过校验签名、有效期、签发者和受众等信息判断令牌是否可信。

  2. 访问授权

    JWT 可以携带用户标识、角色、权限或作用域等声明。服务端完成令牌校验后,再根据这些声明判断当前用户是否可以访问目标资源。

  3. 系统间信息交换

    发送方可以对 JWT 进行签名,使接收方能够验证数据是否被篡改以及令牌是否由可信签发方生成。若业务还要求内容保密,应使用 JWE 或其他加密通道。

  4. 单点登录

    在统一身份认证体系中,认证中心可以签发 JWT,各业务系统根据约定验证令牌。JWT 只是单点登录方案中的一个组成部分,完整方案还需要处理令牌签发、刷新、吊销和信任关系。

JWT 的特点

优点

  • 格式紧凑:适合放在 HTTP 请求头中传输。
  • 跨语言:JWT 是标准化格式,各主流语言均有相应实现。
  • 便于分布式验证:资源服务器可以在本地验证签名,减少对集中式会话存储的依赖。
  • 支持声明扩展:除标准声明外,还可以添加业务自定义声明。

局限

  • 已签发令牌不易立即撤销:通常需要缩短有效期、维护黑名单或使用令牌版本号等机制。
  • 载荷可能泄露信息:JWS 的载荷只是 Base64URL 编码,并未加密。
  • 令牌体积可能较大:声明过多时,每次请求都会增加网络开销。
  • 权限可能过期:若把权限直接写入长时效令牌,数据库权限变更不会立即反映到旧令牌中。

JWT 的结构

常见的签名 JWT 由三部分组成,并使用英文句点连接:

Header.Payload.Signature
image-001
image-001

Header 是一个 JSON 对象,通常包含以下字段:

  • typ:令牌类型,通常为 JWT
  • alg:签名或消息认证码算法,例如 HS256RS256

示例:

{
  "typ": "JWT",
  "alg": "HS256"
}

Header 会经过 Base64URL 编码,形成令牌的第一部分。

Payload

Payload 用于存放声明(Claim)。JWT 规范定义了七个注册声明名称,这些字段均为可选字段。

声明英文名称作用
issIssuer标识令牌签发者
subSubject标识令牌主题,通常代表用户或主体
audAudience标识令牌预期接收方
expExpiration Time标识令牌过期时间
nbfNot Before标识令牌在何时之前不可用
iatIssued At标识令牌签发时间
jtiJWT ID标识令牌的唯一编号

除注册声明外,还可以添加业务自定义声明:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

不要在未加密的 JWT Payload 中存放密码、银行卡号、身份证号、密钥等敏感信息。任何获得令牌的人都可以解码并读取 Header 和 Payload。

Signature

Signature 用于校验 Header 和 Payload 是否被篡改,并验证令牌是否由持有相应密钥的一方签发。

HS256 为例,签名计算过程可以表示为:

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

使用 HMAC 时,签发方与验证方共享同一个密钥;使用 RSA 或 ECDSA 时,签发方使用私钥签名,验证方使用公钥验签。

签名只能保证完整性和来源可信度,不能隐藏 Payload 内容。

Base64URL

Base64URL 是适合 URL 和 HTTP 头部传输的 Base64 变体。

与普通 Base64 相比,它主要进行以下处理:

  • + 替换为 -
  • / 替换为 _
  • 省略末尾的 = 填充字符。

Base64URL 是编码方式,不是加密算法。

使用 JJWT 封装工具类

下面示例基于 JJWT 0.12.x。配置项中的密钥使用 Base64 编码,并且解码后至少应满足所选 HMAC 算法的密钥长度要求。

配置示例

jwt:
  secret: ${JWT_SECRET}
  expire-ms: 86400000

JWT_SECRET 不应直接提交到代码仓库,可通过环境变量或密钥管理服务注入。

JWT 工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.List;

@Component
public class JwtUtil {

    private final SecretKey key;
    private final long expireMs;

    public JwtUtil(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.expire-ms}") long expireMs) {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
        this.expireMs = expireMs;
    }

    public String createToken(
            Long userId,
            String username,
            Long roleId,
            List<String> permissions) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + expireMs);

        return Jwts.builder()
                .subject(username)
                .claim("userId", userId)
                .claim("roleId", roleId)
                .claim("permissions", permissions)
                .issuedAt(now)
                .expiration(expiration)
                .signWith(key)
                .compact();
    }

    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public boolean isExpired(Claims claims) {
        Date expiration = claims.getExpiration();
        return expiration != null && expiration.before(new Date());
    }
}

signWith(key) 会根据密钥类型和长度选择兼容的签名算法。若系统要求固定算法,应在签发和验证两端统一配置,并限制只接受预期算法。

Claims 对象

Claims 表示 JWT 中的声明集合。它继承了 Map<String, Object> 的键值存取能力,同时提供了读取标准声明的专用方法。

常用方法如下:

String subject = claims.getSubject();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
String issuer = claims.getIssuer();

读取自定义声明时,可以指定目标类型:

Long userId = claims.get("userId", Long.class);
Long roleId = claims.get("roleId", Long.class);

对于泛型集合,由于 Java 类型擦除,直接读取时通常只能得到原始 List。可以在业务层逐项转换,或使用自定义反序列化方案。

@SuppressWarnings("unchecked")
List<String> permissions = claims.get("permissions", List.class);

测试令牌的创建与解析

下面的测试在同一个测试方法中创建并解析令牌,避免使用已经过期或被换行破坏的硬编码令牌。

import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

@SpringBootTest
class JwtUtilTests {

    @Autowired
    private JwtUtil jwtUtil;

    @Test
    void shouldCreateAndParseToken() {
        List<String> permissions = List.of(
                "permission:query",
                "permission:insert",
                "permission:update",
                "permission:delete",
                "permission:check"
        );

        String token = jwtUtil.createToken(130L, "tom", 3L, permissions);
        Claims claims = jwtUtil.parseToken(token);

        assertEquals("tom", claims.getSubject());
        assertEquals(130L, claims.get("userId", Long.class));
        assertEquals(3L, claims.get("roleId", Long.class));
        assertFalse(jwtUtil.isExpired(claims));

        @SuppressWarnings("unchecked")
        List<String> parsedPermissions = claims.get("permissions", List.class);
        assertEquals(permissions, parsedPermissions);
    }
}

使用注意事项

  1. 服务端必须验证签名,不能只解码 Payload。
  2. 应校验 exp,并根据业务需要校验 issaudnbf 等声明。
  3. 不要接受客户端自行指定的任意算法,应限制允许的算法和令牌类型。
  4. HMAC 密钥必须足够长且随机,不能使用简单口令代替密钥。
  5. 访问令牌应设置较短有效期;需要长期登录时,应配合刷新令牌机制。
  6. 全程使用 HTTPS,避免令牌在传输过程中被窃取。
  7. 客户端存储方案应根据威胁模型选择,并重点防范 XSS、CSRF 和令牌泄露。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka