JWT令牌
- JWT介绍
- [JWT 的核心结构](#JWT 的核心结构)
- [Java 实操](#Java 实操)
-
- [生成 JWT Token](#生成 JWT Token)
- [解析 JWT Token](#解析 JWT Token)
- 配置JWT的工具类
- 前端存储token的方式
JWT介绍
什么是 JWT
JSON Web Token(JWT,RFC 7519 标准)是一种基于 JSON 格式的轻量级开放标准,用于在网络参与方之间安全传递声明信息,具备紧凑、自包含、可验证的核心属性。
核心特点
- 紧凑性:JWT 采用 Base64URL 编码压缩数据体积,可高效通过 URL 参数、HTTP 请求头或 POST 载荷传输,适配带宽受限的网络场景,且传输效率优于传统会话标识(Session ID);
- 自包含性:Token 内部封装了身份认证、权限声明等核心信息,服务端无需查询数据库或缓存即可完成身份校验,大幅降低服务端存储依赖,尤其适配分布式系统架构;
- 可验证性:JWT 通过数字签名机制保证信息完整性与不可篡改性,支持两种签名方案:
对称加密(如 HS256):使用同一密钥完成签名与验签,适用于信任度高的内部系统;
非对称加密(如 RS256):使用私钥签名、公钥验签,适用于跨信任域的多方通信(如微服务间交互)。
核心应用场景
身份认证(替代传统 Session 机制):用户登录后,服务端生成包含用户身份信息的 JWT 并返回,后续请求通过携带 JWT 完成身份校验,无需服务端存储会话状态,天然适配分布式 / 微服务架构下的单点登录场景(如跨站点、跨应用统一登录);
安全信息交换:在信任的参与方之间传递结构化、可验证的业务声明(如微服务间传递用户权限、接口调用凭证),签名机制确保信息在传输过程中未被篡改,部分场景下可对 Token 内容加密(而非仅签名),实现信息机密性保护。
JWT 的核心结构
JWT 的核心结构由 Header(头部)、Payload(载荷)、Signature(签名)三部分组成,各部分独立完成特定功能,经 Base64URL 编码后通过英文点号(.)连接。
Header(头部)
描述 JWT 元数据(声明 Token 类型和签名算法)
原始 JSON Header:
javascript
{
"alg": "HS256",
"typ": "JWT" // Token类型,固定为JWT
}
Base64URL 编码后结果为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
格式与内容
- 本质是 JSON 对象,需满足 JSON 规范(键值对、字符串值等)
- 必须包含的字段:
typ:固定值为 JWT,声明当前令牌的类型;
alg:指定签名算法,常见值包括:
对称加密:HS256(HMAC-SHA256)、HS384、HS512(单密钥签名 / 验签);
非对称加密:RS256(RSA-SHA256)、RS384、RS512(私钥签名、公钥验签); - 可选字段:如 cty(内容类型,用于嵌套 JWT 时声明)。
编码规则
对 JSON 对象执行 Base64URL 编码(区别于普通 Base64):
替换 + 为 -、/ 为 _(避免 URL/HTTP 头中的特殊字符冲突);移除末尾填充的等号(=),进一步压缩体积。
Payload(载荷)
存储核心声明信息,承载需要在各方之间传递的实际数据,是 JWT 的业务核心
原始 JSON Payload:
javascript
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
Base64URL 编码后结果:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjIsInJvbGUiOiJhZG1pbiJ9
声明类型
按规范约束分类 JWT 定义了三类声明,确保跨系统兼容性:
- 注册声明(Registered Claims):JWT 标准预定义的、具备通用含义的声明(可选但推荐使用),核心字段包括:
iss(Issuer):签发者(如服务端域名);
sub(Subject):主题(通常为用户唯一标识,如用户 ID);
aud(Audience):受众(JWT 的接收方,如特定应用);
exp(Expiration Time):过期时间戳(必须是数字,单位秒,过期后 Token 失效,核心安全字段);
nbf(Not Before):生效时间戳(在此时间前 Token 不可用);
iat(Issued At):签发时间戳;
jti(JWT ID):唯一标识(防止重放攻击)。 - 公有声明(Public Claims):自定义声明,需在 IANA JSON Web Token Registry 中注册,避免字段冲突。
- 私有声明(Private Claims):各方协商的自定义声明,无规范约束,但需避免与注册 / 公有声明重名。
关键约束
Payload 仅通过 Base64URL 编码,任何人获取 JWT 后均可解码查看内容,因此禁止存储敏感信息(如密码、令牌密钥);
若需保护 Payload 内容,需使用 JWE(JSON Web Encryption)标准对 Payload 加密(而非仅签名的 JWS)。
编码规则
对 JSON 对象执行 Base64URL 编码(无填充、字符替换)
Signature(签名)
通过加密算法生成签名,验证 JWT 未被篡改(完整性)且由合法签发者生成(真实性),是 JWT 安全的核心保障。
生成逻辑
- 拼接编码后的 Header 和 Payload:
encodedHeader + "." + encodedPayload; - 使用 Header 中指定的算法(alg),结合密钥对拼接字符串进行签名:
对称加密(HS256 示例):
签名公式 =HMACSHA256(encodedHeader + "." + encodedPayload, secretKey)
(secretKey 为服务端与客户端共享的密钥,需严格保密)
非对称加密(RS256 示例):
签名公式 =RSASHA256(encodedHeader + "." + encodedPayload, privateKey)
(privateKey 为签发方私钥,验签时使用对应公钥); - 对签名结果执行 Base64URL 编码,得到最终的 Signature 部分。
验签过程
服务端接收 JWT 后:
拆分出 Header、Payload、Signature 三部分;
解码 Header 并获取签名算法;
按相同规则拼接 encodedHeader + "." + encodedPayload,使用对应密钥(对称加密用共享密钥,非对称加密用公钥)执行相同算法生成验证签名;
对比 "验证签名" 与 JWT 中的 Signature:若一致,则 Token 未被篡改且签发合法;若不一致,则 Token 无效。
示例(HS256 签名)
拼接字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjIsInJvbGUiOiJhZG1pbiJ9 使用密钥 your-256-bit-secret 执行 HMACSHA256 签名并 Base64URL 编码后,得到 Signature:dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Java 实操
生成 JWT Token
JJWT 是 Java 生态中最常用的 JWT 工具库,基于 JJWT 库生成 JWT 登录令牌
引入依赖(Maven)
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>>0.9.1</version>
</dependency>
jjwt:0.9.1 是 JJWT 早期的单体包,把核心接口、算法实现、JSON 序列化(依赖 Jackson)、异常处理等所有逻辑打包在一个 JAR 里。
引入后会一次性加载所有功能,即使只用到 HS256 签名,也会引入 RS256/ES256 等未使用的算法实现。
xml
<!-- JJWT核心 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- 实现层 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 算法支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
JJWT 从 0.10.x 开始重构为模块化架构,拆分出三个核心依赖:
jjwt-api:核心接口层,仅定义规范,无具体实现,作为编译期依赖(compile 范围);
jjwt-impl:接口的默认运行时实现,仅在运行时生效(runtime 范围),编译期不耦合;
jjwt-jackson:JSON 序列化实现(依赖 Jackson),同样为运行时依赖,可替换为 jjwt-gson/jjwt-orgjson 适配不同序列化框架。
这种设计符合接口与实现分离的原则,便于扩展和维护。
java
public class JwtUtil {
// 密钥(HS256要求密钥长度≥256位)
private static final String SECRET_KEY = "my_256bit_secret_key_123456789012345678901234";
// Token过期时间单位是毫秒
private static final long EXPIRATION_TIME = 3600 * 1000;
// 生成JWT
public static String generateToken(String userId, String username) {
// 1. 构建Header
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
// 2. 构建Payload
return Jwts.builder()
.setHeader(header) // 设置头部
.setSubject(userId) // 注册声明用户ID
.claim("username", username) // 自定义声明用户名
.claim("role", "admin") // 自定义声明角色
.setIssuedAt(new Date()) // 注册声明签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 过期时间
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) // 签名
.compact(); // 生成最终Token
}
// 测试
public static void main(String[] args) {
// 生成Token
String token = generateToken("123456", "张三");
System.out.println("生成的JWT:" + token);
}
}
Jwts.builder() 是核心入口,用于创建 JJWT 库的 JWT 构建器对象;
setHeader(header) 对应 JWT 的 Header,这里使用的是 HS256 对称加密;
setSubject(userId) 对应 Payload 中的注册声明(Registered Claim),设置sub字段,值为用户 ID;
setIssuedAt(new Date()) 对应 Payload 中的注册声明,设置iat字段(Issued At),值为当前系统时间;
setExpiration(...) 对应 Payload 中的注册声明,设置exp字段(Expiration Time),值为当前时间 + 过期时长;
claim("username", username) / claim("role", "admin") 对应 Payload 中的私有声明(Private Claim);
signWith(...) 对应 JWT 的 Signature:这是保证 JWT 不被篡改的核心步骤:
Keys.hmacShaKeyFor(SECRET_KEY.getBytes()):将你的字符串密钥(SECRET_KEY)转换为 HS256 算法所需的密钥对象(必须是 256 位以上的密钥,否则会报错);
SignatureAlgorithm.HS256:指定使用 HS256 对称加密算法签名,服务端验签时,会用相同的 SECRET_KEY 重新计算签名,对比是否和令牌中的签名一致,不一致则说明令牌被篡改,直接拒绝。
compact()最终组装:将 Header、Payload 分别做 Base64URL 编码,再用 HS256 算法生成签名,最后用.连接三部分,生成最终的 JWT 字符串。
若用非对称加密(RS256),需先生成 RSA 公钥 / 私钥,替换签名逻辑:
java
// 加载RSA私钥(生成签名)
PrivateKey privateKey = Keys.privateKeyFor(SignatureAlgorithm.RS256, Files.readAllBytes(Paths.get("private.key")));
// 加载RSA公钥(验证签名)
PublicKey publicKey = Keys.publicKeyFor(SignatureAlgorithm.RS256, Files.readAllBytes(Paths.get("public.key")));
// 生成Token(私钥签名)
Jwts.builder()
.setSubject("123456")
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
// 验证Token(公钥验证)
Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(token);
基于 JJWT 库封装的更模块化的 JWT Token 生成逻辑:
先构建 Claims 对象,通过 Claims 对象构建 token
java
public class JwtUtil {
private static final String SECRET_KEY = "my_256bit_secret_key_123456789012345678901234";
private static final long EXPIRATION_TIME = 3600 * 1000;
/**
* 构建JWT载荷(Payload),构建 Claims 对象
*/
private static Claims buildClaims(String userId, String username) {
// 入参校验
if (userId == null || username == null) {
throw new IllegalArgumentException("userId和username不能为空");
}
// 构建Claims对象(使用DefaultClaims实现类)
Claims claims = new DefaultClaims();
claims.setSubject(userId);
claims.put("username", username);
claims.put("role", "admin");
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME));
return claims;
}
/**
* 生成 JWT Token(先构建载荷Claims,再生成Token)
*/
public static String generateToken(String userId, String username) {
// 先构建载荷Claims
Claims claims = buildClaims(userId, username);
// 生成安全的签名密钥
JwsHeader header = Jwts.header()
.add("alg", "HS256")
.add("typ", "JWT")
.build();
// 通过Claims构建最终Token
return Jwts.builder()
.setHeader(header) // 设置Header
.setClaims(claims) // 绑定预构建的Claims载荷
.signWith( // 签名(新版JJWT兼容写法)
Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
SignatureAlgorithm.HS256
)
.compact(); // 生成最终Token字符串
}
}
解析 JWT Token
解析的本质是反向验证生成 Token 的过程
生成时是「构建 Header/Payload→签名→拼接」,解析时是「拆分→验签→解码→校验→提取」
java
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
// 验证并解析JWT
public static Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) // 设置验证密钥
.build() // 构建JWT解析器对象
.parseClaimsJws(token) // 解析Token
.getBody(); // 获取Payload
} catch (ExpiredJwtException e) {
throw new RuntimeException("Token已过期");
} catch (UnsupportedJwtException e) {
throw new RuntimeException("不支持的JWT格式");
} catch (MalformedJwtException e) {
throw new RuntimeException("JWT格式错误");
} catch (SignatureException e) {
throw new RuntimeException("JWT签名验证失败");
} catch (IllegalArgumentException e) {
throw new RuntimeException("JWT为空或无效");
}
}
// 测试
public static void main(String[] args) {
// 解析Token
Claims claims = parseToken(token);
System.out.println("用户ID:" + claims.getSubject());
System.out.println("用户名:" + claims.get("username"));
System.out.println("过期时间:" + claims.getExpiration());
}
}
Jwts.parserBuilder() 用于初始化 JWT解析器构建器;
setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) 设置验签密钥;
build() 根据前面配置的验签密钥构建出最终的 JWT 解析器对象(JwtParser),此时解析器已具备验证能力;
parseClaimsJws(token) 解析传入的 JWT 字符串(token),并自动完成多维度的合法性校验,只有全部校验通过才会继续,否则直接抛出异常。
校验通过后,返回 Jws<Claims> 对象,这个对象包含了解析后的完整 JWT 数据:
getHeader() 获取 JWT 的 Header 部分,getBody() 获取 JWT 的 Payload 部分(Claims 类型的对象),getSignature() 获取 JWT 的 Signature 部分。
getSubject() 是 Claims 类为标准注册声明 sub 提供的快捷方法,等价于 claims.get("sub");
getExpiration() 是 Claims 为标准注册声明 exp 提供的快捷方法,返回 Date 类型的过期时间(等价于 new Date((Long) claims.get("exp"))),直接打印会输出 Date 的默认字符串格式(如 Tue Dec 23 16:30:00 CST 2025);
从 Claims 对象中提取自定义私有声明的值 Claims 没有专门的快捷方法,所以用通用的 get(String key) 方法,传入声明的键名即可获取对应值,注意返回值是 Object 类型,若需要特定类型,建议用重载方法如 claims.get("username", String.class),避免后续类型转换问题。
异常捕获:
UnsupportedJwtException:当 token 不是通过 Claims 对象构建的 token 时;
ExpiredJwtException:当 token 已过期时;
MalformedJwtException:当 token 不是有效的 Claims 对象构建的 token 时;
SignatureException:当 token 的 Signature 验证失败时;
IllegalArgumentException:当 token 为 null 或 token 是空字符串或 token 中只有空字符时;
配置JWT的工具类
基于 JJWT 库封装的、适配 Spring 业务场景的业务级工具类
java
@Component
public class JwtTokenUtils
{
private static final String CLAIM_KEY_USERNAME = "user";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 工具方法:用于生成过期时间
* */
private Date generateExpirationDate()
{
return new Date(System.currentTimeMillis() + expiration*1000);
}
/**
* 工具方法:用于通过token获取荷载
*/
private Claims getClaimsFormToken(String token)
{
Claims claims = null;
try
{
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
catch (Exception e)
{
e.printStackTrace();
}
return claims;
}
/**
* 工具方法:用于通过token获取失效时间
* */
private Date getExpiredTimeFromToken(String token)
{
Claims claims = getClaimsFormToken(token);
return claims.getExpiration();
}
/**
* 工具方法:用于判断token是否达到失效时间
* 1)如果过期时间早于当前时间,说明token已失效
* 2)如果过期时间晚于当前时间,说明token未失效
* */
private Boolean isTokenExpired(String token)
{
Date expirted = getExpiredTimeFromToken(token);
return expirted.before(new Date());
}
/**
* 1.根据信息生成token
* */
public String generateToken(UserDetails details)
{
/*第一步:构建荷载用于存放token的容器*/
Map<String, Object> claim = new HashMap<>();
claim.put(CLAIM_KEY_USERNAME, details.getUsername());
claim.put(CLAIM_KEY_CREATED, new Date());
/**
* 第二步:通过荷载构建token,并返回
* */
return Jwts.builder()
.setClaims(claim)//设置荷载
.setExpiration(generateExpirationDate())//设置过期时间
.signWith(SignatureAlgorithm.HS512, secret)//设置签名算法
.compact();//该方法用于生成token
}
/**
* 2.外界调用方法:根据token获取用户名
*/
public String getUserNameFromToken(String token)
{
/*
* 第一步:从token中获取荷载
* */
String username = null;
try
{
Claims claims = getClaimsFormToken(token);
username = claims.getSubject();
}catch (Exception e)
{
username = null;
}
return username;
}
/**
* 3.外界调用方法:判断token是否有效。
* */
public Boolean validateToken(String token, UserDetails userDetails)
{
return getUserNameFromToken(token).equals(userDetails.getUsername()) && !isTokenExpired(token);
}
}
前端存储token的方式
token存储在了cookie与请求头中:

后端打印所有请求头信息:
java
@RestController
public class LoginController {
@GetMapping("/checkToken")
public Result login(HttpServletRequest request) {
System.out.println("======= 请求头(Headers) =======");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
System.out.println(headerName + ": " + headerValue);
}
}
