JWT
JWT 概述
JWT(JSON Web Token)是一种紧凑、URL 安全的声明传输格式,常用于在不同系统之间传递身份与授权相关信息。
JWT 本身是一种令牌格式,并不等同于登录协议或单点登录方案。它可以作为认证、授权和单点登录流程中的一种令牌载体。
JWT 是否保密取决于具体封装方式。常见的三段式 JWT 属于 JWS,仅提供签名或消息认证码保护,载荷内容仍可被读取;只有使用 JWE 时,载荷才会被加密。
JWT 的常见应用场景
-
身份认证
用户登录成功后,服务端签发 JWT。客户端在后续请求中携带该令牌,服务端通过校验签名、有效期、签发者和受众等信息判断令牌是否可信。
-
访问授权
JWT 可以携带用户标识、角色、权限或作用域等声明。服务端完成令牌校验后,再根据这些声明判断当前用户是否可以访问目标资源。
-
系统间信息交换
发送方可以对 JWT 进行签名,使接收方能够验证数据是否被篡改以及令牌是否由可信签发方生成。若业务还要求内容保密,应使用 JWE 或其他加密通道。
-
单点登录
在统一身份认证体系中,认证中心可以签发 JWT,各业务系统根据约定验证令牌。JWT 只是单点登录方案中的一个组成部分,完整方案还需要处理令牌签发、刷新、吊销和信任关系。
JWT 的特点
优点
- 格式紧凑:适合放在 HTTP 请求头中传输。
- 跨语言:JWT 是标准化格式,各主流语言均有相应实现。
- 便于分布式验证:资源服务器可以在本地验证签名,减少对集中式会话存储的依赖。
- 支持声明扩展:除标准声明外,还可以添加业务自定义声明。
局限
- 已签发令牌不易立即撤销:通常需要缩短有效期、维护黑名单或使用令牌版本号等机制。
- 载荷可能泄露信息:JWS 的载荷只是 Base64URL 编码,并未加密。
- 令牌体积可能较大:声明过多时,每次请求都会增加网络开销。
- 权限可能过期:若把权限直接写入长时效令牌,数据库权限变更不会立即反映到旧令牌中。
JWT 的结构
常见的签名 JWT 由三部分组成,并使用英文句点连接:
Header.Payload.Signature

Header
Header 是一个 JSON 对象,通常包含以下字段:
typ:令牌类型,通常为JWT。alg:签名或消息认证码算法,例如HS256、RS256。
示例:
{
"typ": "JWT",
"alg": "HS256"
}
Header 会经过 Base64URL 编码,形成令牌的第一部分。
Payload
Payload 用于存放声明(Claim)。JWT 规范定义了七个注册声明名称,这些字段均为可选字段。
| 声明 | 英文名称 | 作用 |
|---|---|---|
iss | Issuer | 标识令牌签发者 |
sub | Subject | 标识令牌主题,通常代表用户或主体 |
aud | Audience | 标识令牌预期接收方 |
exp | Expiration Time | 标识令牌过期时间 |
nbf | Not Before | 标识令牌在何时之前不可用 |
iat | Issued At | 标识令牌签发时间 |
jti | JWT 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);
}
}使用注意事项
- 服务端必须验证签名,不能只解码 Payload。
- 应校验
exp,并根据业务需要校验iss、aud、nbf等声明。 - 不要接受客户端自行指定的任意算法,应限制允许的算法和令牌类型。
- HMAC 密钥必须足够长且随机,不能使用简单口令代替密钥。
- 访问令牌应设置较短有效期;需要长期登录时,应配合刷新令牌机制。
- 全程使用 HTTPS,避免令牌在传输过程中被窃取。
- 客户端存储方案应根据威胁模型选择,并重点防范 XSS、CSRF 和令牌泄露。
喜欢的话,留下你的评论吧~