纲要
- JWT 核心概念
- 什么是
JWT(JSON Web Token) - 自包含、紧凑、可信赖的特性
- 什么是
- JWT 工作流程
- 登录签发流程
- 后续请求验证流程
- JWT 数据结构
- Header:签名算法
- Payload:标准声明与自定义声明
- Signature:签名与验证
- 实战:在 Spring Security 环境中生成与解析 JWT
- 项目依赖配置
- 工具类
JwtUtil实现 - 单元测试验证
- 安全注意事项
- 总结
JWT 核心概念
JWT 是 JSON Web Token 的缩写,基于 RFC 7519 标准,是一种用于安全传输信息的紧凑、自包含的 JSON 对象。紧凑意味着它体积小,可以通过 URL、POST 参数或 HTTP 头部传输;自包含是指载荷中包含了所有必要的信息,避免了服务端再次查询数据库的开销。搭配数字签名后,令牌可以被信任和验证,非常适合前后端分离架构下的无状态认证。
JWT 工作流程
整个认证与授权过程可以划分为两个阶段:登录签发 与 请求验证。
服务端 客户端 服务端 客户端 #mermaid-svg-9VuHAB6pmB7NCpeH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9VuHAB6pmB7NCpeH .error-icon{fill:#552222;}#mermaid-svg-9VuHAB6pmB7NCpeH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9VuHAB6pmB7NCpeH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9VuHAB6pmB7NCpeH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9VuHAB6pmB7NCpeH .marker.cross{stroke:#333333;}#mermaid-svg-9VuHAB6pmB7NCpeH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9VuHAB6pmB7NCpeH p{margin:0;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9VuHAB6pmB7NCpeH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .sequenceNumber{fill:white;}#mermaid-svg-9VuHAB6pmB7NCpeH #sequencenumber{fill:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9VuHAB6pmB7NCpeH .messageText{fill:#333;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH .labelText,#mermaid-svg-9VuHAB6pmB7NCpeH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .loopText,#mermaid-svg-9VuHAB6pmB7NCpeH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9VuHAB6pmB7NCpeH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9VuHAB6pmB7NCpeH .noteText,#mermaid-svg-9VuHAB6pmB7NCpeH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9VuHAB6pmB7NCpeH .actorPopupMenu{position:absolute;}#mermaid-svg-9VuHAB6pmB7NCpeH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9VuHAB6pmB7NCpeH .actor-man circle,#mermaid-svg-9VuHAB6pmB7NCpeH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9VuHAB6pmB7NCpeH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后续请求 POST /login (用户名+密码)校验凭证,查询数据库返回 JWT TokenAPI 请求 (Header: Authorization: Bearer <JWT>)验证签名,解析载荷响应数据
- 客户端提交用户名、密码进行登录。
- 服务端验证凭证后,从数据库加载用户信息,生成包含用户标识、权限等信息的 JWT,返回给客户端。
- 客户端在后续所有请求的
Authorization头中携带该 Token。 - 服务端拦截请求,验证 JWT 的签名是否合法,并从载荷中直接获取用户身份与权限,无需再次查询数据库。
- 验证通过后,将请求的资源返回。
JWT 数据结构
一个典型的 JWT 字符串如下所示,由两个小数点分割为三部分:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwiZXhwIjoxNjgxMjM0NTY3LCJpYXQiOjE2ODEyMzQ1MDd9.签名部分
#mermaid-svg-qJHAB2THRdlap0vd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qJHAB2THRdlap0vd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qJHAB2THRdlap0vd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qJHAB2THRdlap0vd .error-icon{fill:#552222;}#mermaid-svg-qJHAB2THRdlap0vd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qJHAB2THRdlap0vd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qJHAB2THRdlap0vd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qJHAB2THRdlap0vd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .marker.cross{stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qJHAB2THRdlap0vd p{margin:0;}#mermaid-svg-qJHAB2THRdlap0vd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label text{fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label span{color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster-label span p{background-color:transparent;}#mermaid-svg-qJHAB2THRdlap0vd .label text,#mermaid-svg-qJHAB2THRdlap0vd span{fill:#333;color:#333;}#mermaid-svg-qJHAB2THRdlap0vd .node rect,#mermaid-svg-qJHAB2THRdlap0vd .node circle,#mermaid-svg-qJHAB2THRdlap0vd .node ellipse,#mermaid-svg-qJHAB2THRdlap0vd .node polygon,#mermaid-svg-qJHAB2THRdlap0vd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .rough-node .label text,#mermaid-svg-qJHAB2THRdlap0vd .node .label text,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label,#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label{text-anchor:middle;}#mermaid-svg-qJHAB2THRdlap0vd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .rough-node .label,#mermaid-svg-qJHAB2THRdlap0vd .node .label,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label,#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label{text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .node.clickable{cursor:pointer;}#mermaid-svg-qJHAB2THRdlap0vd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .arrowheadPath{fill:#333333;}#mermaid-svg-qJHAB2THRdlap0vd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qJHAB2THRdlap0vd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qJHAB2THRdlap0vd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qJHAB2THRdlap0vd .cluster text{fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd .cluster span{color:#333;}#mermaid-svg-qJHAB2THRdlap0vd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qJHAB2THRdlap0vd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qJHAB2THRdlap0vd rect.text{fill:none;stroke-width:0;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape,#mermaid-svg-qJHAB2THRdlap0vd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape p,#mermaid-svg-qJHAB2THRdlap0vd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qJHAB2THRdlap0vd .icon-shape .label rect,#mermaid-svg-qJHAB2THRdlap0vd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qJHAB2THRdlap0vd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qJHAB2THRdlap0vd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qJHAB2THRdlap0vd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Base64 解码
Base64 解码
JWT Token
Header
Payload
Signature
alg: HS512
sub: admin, exp, iat...
密钥对 Header+Payload 签名
| 部分 | 内容 | 说明 |
|---|---|---|
| Header | {"alg":"HS512"} |
说明签名算法,这里使用 HMAC-SHA512 |
| Payload | {"sub":"admin","authorities":["ROLE_USER"],"exp":1681234567,"iat":1681234507} |
存放声明(Claims),包含标准字段和自定义字段 |
| Signature | 签名结果 | 使用 Header 中指定的算法,对 Base64 编码后的 Header 和 Payload 进行签名,保证完整性 |
常用标准声明(Registered Claims)
| 字段 | 全称 | 说明 |
|---|---|---|
sub |
Subject | 主题,通常为用户标识 |
exp |
Expiration | 过期时间,UNIX 时间戳 |
iat |
Issued At | 签发时间 |
iss |
Issuer | 签发者 |
aud |
Audience | 受众 |
重要 :Header 和 Payload 仅经过 Base64 编码,并非加密,因此绝对不能存放密码、密钥等敏感信息。安全性完全由最后的签名保证。
实战:集成 JJWT 库
在 Java 生态中,JJWT 是使用最广泛的 JWT 库,支持服务端和 Android 端。下面演示如何在 Spring Boot 项目中引入依赖,并编写工具类实现 JWT 的创建与解析。
项目依赖
使用 Maven 时,在 pom.xml 中添加:
xml
<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>
若使用 Gradle,可对应转换。这三个模块分别提供 API、实现以及 Jackson 序列化支持。
项目结构
dir
src
└── main
└── java
└── com
└── example
└── security
├── util
│ └── JwtUtil.java
└── model
└── (Spring Security UserDetails 实现)
工具类实现:JwtUtil
该类负责根据 UserDetails 生成 JWT,并提供解析方法。需要注意的是,密钥在实际项目中应从配置中读取,这里为演示方便定义为静态常量。
java
package com.example.security.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
public class JwtUtil {
// 使用 HS512 算法生成一个符合长度要求的密钥,生产环境请从外部配置读取
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// Token 有效期,单位:毫秒(示例设为 1 小时)
private static final long EXPIRATION_MS = 3600_000;
/**
* 根据 UserDetails 生成 JWT Token
* @param userDetails 用户信息
* @return JWT 字符串
*/
public static String generateToken(UserDetails userDetails) {
// 提取权限列表,转换为纯字符串集合
List<String> authorities = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
Date now = new Date();
Date expiration = new Date(now.getTime() + EXPIRATION_MS);
return Jwts.builder()
.setId(userDetails.getUsername() + "_" + System.currentTimeMillis()) // JWT ID
.claim("authorities", authorities) // 自定义声明:权限列表
.setSubject(userDetails.getUsername()) // 主题:用户名
.setIssuedAt(now) // 签发时间
.setExpiration(expiration) // 过期时间
.signWith(SECRET_KEY, SignatureAlgorithm.HS512) // 签名算法与密钥
.compact();
}
/**
* 解析 JWT 并返回 Claims
* @param token JWT 字符串
* @return 包含声明的 Claims 对象
*/
public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 从 Token 中提取用户名
*/
public static String getUsernameFromToken(String token) {
return parseToken(token).getSubject();
}
/**
* 验证 Token 是否过期
*/
public static boolean isTokenExpired(String token) {
return parseToken(token).getExpiration().before(new Date());
}
}
单元测试
以下测试验证 Token 的生成与解析是否正确,确保载荷中的 subject 和自定义 authorities 与原始数据一致。
java
package com.example.security.util;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class JwtUtilTest {
@Test
void shouldGenerateAndParseTokenSuccessfully() {
// 1. 构造 UserDetails 对象
UserDetails user = new User("admin", "password",
List.of(new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_ADMIN")));
// 2. 生成 JWT
String token = JwtUtil.generateToken(user);
assertNotNull(token);
// 3. 解析 JWT
var claims = JwtUtil.parseToken(token);
// 4. 验证主题(用户名)
assertEquals("admin", claims.getSubject());
// 5. 验证自定义权限字段
List<String> authorities = claims.get("authorities", List.class);
assertNotNull(authorities);
assertTrue(authorities.contains("ROLE_USER"));
assertTrue(authorities.contains("ROLE_ADMIN"));
// 6. 验证签发时间与过期时间
assertNotNull(claims.getIssuedAt());
assertNotNull(claims.getExpiration());
assertTrue(claims.getExpiration().after(claims.getIssuedAt()));
}
}
运行测试,若全部通过,说明 JWT 的生成与解析逻辑正确。
安全注意事项
- 密钥管理:示例中将密钥硬编码在代码中,仅用于演示。实际项目应通过配置文件、环境变量或密钥管理服务注入,并对密钥进行严格的访问控制。
- 算法选择:HMAC-SHA512 适用于单体或内部服务。在分布式、多服务验签场景下,推荐使用非对称算法(如 RSA 或 ECDSA),由授权服务器持有私钥,资源服务器使用公钥验证。
- 载荷敏感数据:切勿将密码、令牌 Secret 等敏感信息放入 Payload,因为 Base64 编码极易被解码。
- 过期时间 :务必设置合理的
exp声明,并配合刷新令牌机制,降低 Token 泄漏后的风险。
总结
本文从 JWT 的基本概念入手,分析了其自包含、紧凑、可签名的特性,并通过时序图展现了在前后端分离架构下的工作流程。在数据结构部分,我们拆解了 Header、Payload、Signature 三部分的构成与作用,并强调了安全禁忌。
随后,在 Spring Security 技术栈下,基于 JJWT 库完成了依赖引入、工具类编写以及单元测试,完整展示了 JWT 的签发与解析逻辑。掌握这些核心内容,将为后续深入学习 OAuth2 + JWT 的实战奠定坚实基础。