前言: 本文将简单介绍一下JWT令牌机制
一 , Cookie-Session 机制及其局限性
在Web开发中,由于HTTP协议的无状态特性, 为了使得每次客户端连接时服务器都能识别出用户, 我们引入了Cookie和Session来为HTTP请求添加了状态
1. 核心工作原理
-
认证 (Authentication):用户提交账号密码,服务器验证通过。
-
状态存储 (Stateful) :服务器在内存/内存数据库(如 Redis)中生成一个唯一的
Session ID,并以此为 Key 存储用户信息。 -
客户端携带 :服务器通过 HTTP 响应头
Set-Cookie将Session ID写入浏览器。后续请求浏览器会自动在 Header 中携带该 Cookie。

2. 核心局限性 (Limitations)
这种机制在单体架构时代很完美,但在现代互联网架构中,它有三个致命局限:
-
服务器开销大(状态留存): Session 是有状态(Stateful)的。当用户量暴增时,服务器需要消耗大量内存来存储 Session。
-
不适合分布式和微服务架构(扩展性差): 在多台服务器(集群)或微服务架构下,用户的请求可能会被负载均衡器分配到不同的服务器上。如果服务 A 存了 Session,服务 B 没有,用户就会"莫名其妙"地掉线。虽然可以通过 Session 复制、Session 粘滞或 Redis 集中存储来解决,但这也增加了系统的复杂性和对外部缓存的依赖。
-
跨域与前后端分离的限制: 现代前端通常和后端部署在不同的域名下。Cookie 在跨域请求时会遇到诸多限制(如同源策略、SameSite 属性等)。此外,移动端(App、小程序)对 Cookie 的支持并不友好。
-
CSRF(跨站请求伪造)风险: 因为浏览器会自动携带 Cookie,黑客很容易利用这一点进行 CSRF 攻击。
二, 为什么引入 JWT?
为了解决上述问题,JWT(JSON Web Token) 应运而生。
JWT 的核心思想是:将状态从服务器端转移到客户端。它是一种无状态(Stateless)的认证机制。
服务器不再存储任何 Session 数据,而是把用户信息加密/签名成一个字符串(Token),直接发给客户端。客户端自己保存这个 Token,每次请求时手动放在 HTTP 请求头中传给服务器。服务器只需要"解密/验签"这个 Token,就知道是谁在访问了。
打个比方:
Session 模式: 游乐园发给你一张会员卡号,游乐园内部的电脑里记录着这个卡号对应是谁、买了什么票。每次你进去,服务员都要查电脑。
JWT 模式: 游乐园直接发给你一张盖了防伪钢印的纸质门票,上面写着你的名字和有效时间。服务员只要肉眼验一下钢印真伪,不需要查电脑就能放行。

三, JWT 的结构与原理
JWT 是一个很长的字符串,由英文句点 . 分隔成三个部分:Header.Payload.Signature(头部.负载.签名)。
① Header(头部)
通常包含两部分信息:Token 的类型(这里是 JWT)和使用的签名算法(如 HMAC SHA256 或 RSA)。
JSON
{
"alg": "HS256",
"typ": "JWT"
}
这段 JSON 会被使用 Base64URL 算法转换为字符串。
② Payload(负载)
这里存放实际需要传输的用户信息(Claims),比如用户 ID、用户名、过期时间等。
JSON
{
"sub": "1234567890",
"name": "John Doe",
"exp": 1716715200
}
同样,这段 JSON 也会被 Base64URL 转换为字符串。
⚠️ 极重要注意: Base64 是一种编码方式,不是加密 !这意味着任何人拿到 JWT 都可以解码并看到 Payload 里的内容。因此,绝对不能在 Payload 中存放密码、银行卡号等敏感信息 。
如果在payload中存放用户的密码的话,就好比你把"秘密"翻译为了Secret,虽然可能不认识英语的人无法知道这个单词是什么含义,但是一旦有人了解Base64编码方式,那么你自认为的加密行为就形同虚设~~
③ Signature(签名)
这是 JWT 最关键的安全保障。它是对前两部分(Header 和 Payload)进行签名的结果。
相当于是基于头部(header) 和载荷(payload) 进行了一次加密
服务器有一个只有自己知道的 密钥(Secret)。生成签名的公式如下:
Signature=HMACSHA256(base64UrlEncode(Header)+"."+base64UrlEncode(Payload),secret)\text{Signature} = \text{HMACSHA256}(\text{base64UrlEncode}(\text{Header}) + "." + \text{base64UrlEncode}(\text{Payload}), \text{secret})Signature=HMACSHA256(base64UrlEncode(Header)+"."+base64UrlEncode(Payload),secret)

防篡改机制:
如果黑客尝试在客户端修改 Payload 里的用户 ID(比如把用户 ID 从 100 改成 1 企图变成管理员),由于黑客不知道服务器的 secret,他无法重新生成正确的签名。当服务器收到被篡改的 JWT 时,用自己的密钥一验算,就会发现签名对不上,从而拒绝请求。
四, JWT 的完整认证机制(工作流程)
-
登录: 用户在浏览器输入账号密码,发送 POST 请求到后端。
-
生成: 后端验证账号密码正确后,将用户 ID 等非敏感信息作为 Payload,加上服务器私有的 Secret 密钥,生成一个 JWT 字符串。
-
返回: 后端将这个 JWT 返回给前端(可以放在响应体中)。
-
存储: 前端收到 JWT 后,将其存储在
localStorage、sessionStorage或Cookie中。 -
携带请求: 之后前端每次向后端请求数据时,都会在 HTTP 请求头中加入
Authorization字段,格式通常为:Authorization: Bearer <Your_JWT_Token> -
验签放行: 后端收到请求后,不需要查数据库或 Redis,只需直接取出请求头中的 JWT,用本地的 Secret 密钥验证其签名是否有效、是否过期。如果有效,直接从 Payload 中获取用户 ID,处理业务并返回数据。

五, JWT实操演练
下面是在项目中使用JWT的代码案例
在项目中,我们通常会封装一个全局工具类来处理 JWT 的生成 与解析 。下面是基于最新版 io.jsonwebtoken (JJWT) 库实现的完整工具类:
在使用JWT前,我们需要引入相应的依赖
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<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> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
对于生成令牌和校验令牌这种操作,我们一般构建一个工具类JwtUtils来封装这些操作
java
public class JwtUtils {
//密钥,HS256 算法要求密钥的长度至少为 256 位(32 字节),SECRET不能太短
private static final String SECRET = "spring-blog-demo-jwt-secret-key-2026-amadeus";
//过期时间
private static final long EXPIRE_TIME = 1000L * 60 * 60 * 24;
//安全密钥
private static final Key KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
private JwtUtils() {
}
//生成JWT令牌
public static String genJwt(Map<String, Object> claims) {
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + EXPIRE_TIME))
.signWith(KEY, SignatureAlgorithm.HS256)
.compact();
}
//解析JWT令牌
public static Claims parseJwt(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException | IllegalArgumentException e) {
return null;
}
}
}
在 genJwt 方法中,通过 Jwts.builder() 采用建造者模式进行链式配置。
-
setClaims(claims):将业务数据(如用户ID、角色等)打包进 Token。 -
setExpiration(...):设定了 24 小时后自动失效,确保了即使 Token 被窃取,其生命周期也是有限的。
在parseJwt方法中,JWT 的验证不仅是解密,更重要的是防篡改 和防过期。
-
如果客户端传来的 Token 被修改过任何一个字符,
parseClaimsJws(token)会立刻察觉并抛出SignatureException。 -
如果当前时间已经超过了
Expiration设定的时间,它会抛出ExpiredJwtException。 -
代码通过
catch (JwtException | IllegalArgumentException e)将这些异常统一拦截并返回null,避免了异常向上抛出导致前端收到 500 错误,让业务层鉴权逻辑更平滑。
项目中如何实际应用?
有了这个工具类后,在 Spring Boot 项目中通常有两个核心应用场景:
1.场景 A:用户登录成功后颁发 Token
在 Login 业务中,校验完用户名和密码后,将用户基本信息存入 Map 并生成 Token 返回给前端。
Java
java
Map<String, Object> claim = new HashMap<>();
claim.put("userId", userInfo.getId());
claim.put("userName", userInfo.getUserName());
return new UserLoginResponse(userInfo.getId(), JwtUtils.genJwt(claim));
这里我使用UserLoginResponse 来统一登录的返回类型
java
public class UserLoginResponse {
private Integer userId;
private String token;
}
3.场景 B:在拦截器(Interceptor)中进行鉴权
创建一个 LoginInterceptor,对受保护的 API 进行拦截。从请求头 Authorization 中获取 Token 并解析。
Java
java
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
log.warn("request path: {}, missing token", request.getRequestURI());
throw new BlogException("请先登录");
}
Claims claims = JwtUtils.parseJwt(token);
if (claims == null) {
log.warn("request path: {}, invalid token", request.getRequestURI());
throw new BlogException("登录已过期,请重新登录");
}
request.setAttribute("userId", claims.get("userId", Integer.class));
request.setAttribute("userName", claims.get("userName", String.class));
return true; }
}
生成令牌和解析令牌的工作都已完成, 接下来我们可以创建一个测试类来观察效果

java
@SpringBootTest
public class JWTTest {
// 24小时过期时间
private final long Expiration = 24 * 60 * 60 * 1000;
// 预先生成的安全的 Base64 密钥字符串
private final String secretString = "rXRoWn8J+bCcnadUFU88AIqhK9caLJCg+6c2rP8F1EE=";
// 根据密钥字符串生成签名需要的 Key 对象
private final Key key = Keys.hmacShaKeyFor(secretString.getBytes());
/**
* 测试用例 1:生成安全的随机密钥
* 生产环境的 SECRET 字符串应该由此方法生成
*/
@Test
public void genKey(){
// 自动生成符合 HS256 算法安全强度的 SecretKey SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 将其转化为高度不可读、不可逆向猜测的 Base64 编码字符串
String encode = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println("生成的安全密钥 -> " + encode);
}
/**
* 测试用例 2:测试 JWT 令牌的生成
*/
@Test
public void genJwt(){
Map<String, Object> claim = new HashMap<>();
claim.put("id", 1);
claim.put("name", "cheems"); // 放入我们可爱的测试用户 cheems
String compact = Jwts.builder()
.setClaims(claim)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + Expiration)) // 设置有效期
.signWith(key) // 签名
.compact();
System.out.println("生成的 Token -> " + compact);
}
/**
* 测试用例 3:测试 JWT 令牌的解析与合法性校验
*/
@Test
public void parseJwt(){
// 传入一个待验证的 Token 字符串
String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiY2hlZW1zIiwiaWQiOjEsImlhdCI6MTc3OTc5NzYwMywiZXhwIjoxNzc5ODg0MDAzfQ.EDXyOGB0iY96-xuv1pJCzVri81CZzLiervjboE4G6P8";
// 1. 创建解析器,并注入相同的签名密钥
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(key);
// 2. 解析并获取 Payload 载荷
Claims body = jwtParserBuilder.build().parseClaimsJws(token).getBody();
System.out.println("解析出的数据 -> " + body);
}
}

那么这里测试类中的genKey方法有何作用呢?
Siginature 的 计算公式为:
Signature=HMACSHA256(base64UrlEncode(Header)+"."+base64UrlEncode(Payload),secret)\text{Signature} = \text{HMACSHA256}(\text{base64UrlEncode}(\text{Header}) + "." + \text{base64UrlEncode}(\text{Payload}), \text{secret})Signature=HMACSHA256(base64UrlEncode(Header)+"."+base64UrlEncode(Payload),secret)
使用genKey生成的 secret 随机且不同 ,从而使得 JWT的第三部分Signature(签名)不同
假设有两个不同的网站(网站 A 和 网站 B),它们都使用 JWT 存了同样的一句话:{"user": "cheems"}。
-
网站 A 的密钥是:
secret-key-AAAAA... -
网站 B 的密钥是:
secret-key-BBBBB...
虽然它们的前两部分(Header 和 Payload)由于数据相同,Base64 编码出来的内容一模一样;但因为密钥不同 ,经过加密算法计算后,网站 A 和网站 B 生成的第三部分(签名)会截然不同。
这样就实现了隔离:网站 A 颁发的 Token,绝对没办法去登录网站 B。
同时如果坏人把 Payload 改成了 {"userId": 2}(哪怕只改了一个数字!),在密钥不变的情况下,重新计算出的第三部分会变成 Y(跟 X 没有任何相似之处)。
使用genKey() 就可以替换掉先前我们在JwtUtil中硬编码的密钥字符串了

除此之外,我们还可以把开发阶段生成的JWT密钥设置在配置文件中,以便于妥善保管,注意不要泄露哦~~
总而言之,这就是genKey的主要功能 : 生成一个随机且安全的JWT签名密钥
六, JWT 就完美无缺了吗?
虽然 JWT 解决了 Session 的扩展性问题,但它也不是万能药,同样存在局限:
-
无法单点注销(一旦发出,覆水难收): 因为服务器不保存状态,如果用户点击了"退出登录",或者管理员想强制某个违规用户下线,服务器很难让一个未过期的 JWT 立即失效。通常需要引入 Redis 黑名单机制来配合解决(但这又让系统变重了)。
-
体积较大: JWT 包含的信息较多,编码后字符串很长,每次请求都带着它,会消耗更多的网络带宽。
-
续签麻烦: JWT 一旦过期,用户就必须重新登录。为了用户体验,通常需要引入双 Token 机制(
AccessToken短期用于请求,RefreshToken长期用于刷新 Token)。
双Token
什么是双Token机制呢?假设你进出一家高安全级别的公司:
-
Access Token(访问令牌) :就像是一张"临时通行纸条"。上面写着你的名字,保安看一眼就放行。为了安全,这张纸条半小时就过期。
-
Refresh Token(刷新令牌) :就像是你的"员工大卡(工作证)"。它不能直接塞给大楼保安看,但当你的"临时通行纸条"过期时,你可以拿着"员工大卡"去 HR 部门 免费以旧换新,重新领一张半小时有效的纸条。这张大卡通常可以维持 30 天有效。
双 Token 的核心工作流程
在前端和后端的交互中,它们的配合非常默契,用户在这个过程中几乎是完全无感知的:
-
用户登录 :用户输入账号密码,服务器验证成功,一口气颁发两个 Token(
Access Token和Refresh Token)给前端。 -
正常访问 :前端之后的每次请求,都只带上
Access Token。服务器验证它有效,正常返回数据。 -
Access Token 过期 :过了 30 分钟,
Access Token失效了。前端再次发起请求时,服务器返回一个特殊的错误码(通常是401 Unauthorized)。 -
静默刷新 :前端的拦截器(如 Axios Interceptor)拦截到这个 401 错误,不惊动用户 ,自动在后台把
Refresh Token发送给服务器的一个"刷新接口"。 -
续命成功 :服务器检查
Refresh Token有效,生成一个全新 的Access Token给前端。 -
重新请求 :前端拿到新的
Access Token,重新发送刚才失败的那个请求。用户只觉得网络可能稍微卡了一下下,完全不需要重新输入密码。
为什么要这么麻烦?直接把 JWT 有效期设长一点不行吗?
如果只用一个 Token,把有效期设为 30 天,确实没有续签麻烦了,但会带来灾难性的安全隐患:
-
JWT 的"泼水难收"特性 :JWT 是无状态的,服务器不保存它的状态。这意味着,一旦一个 JWT 被黑客窃取,只要它没过期,黑客就可以在任意地方冒充用户,服务器对此无能为力(无法主动让其失效)。
-
双 Token 的降维打击:
-
Access Token频繁在网络上传输,被窃取的风险高。但因为它寿命极短(如15分钟),黑客偷过去还没捂热就过期了,损失可控。 -
Refresh Token只在刷新时传输一次 ,平时静静地躺在安全的存储里(如浏览器的HttpOnlyCookie 中,JS 代码无法读取,免疫 XSS 攻击)。 -
支持主动拉黑 :因为
Refresh Token数量少且只用于刷新,服务器通常会在数据库或 Redis 里记录它。如果用户换了设备、修改了密码或者发现异常,服务器可以随时在后端撕毁(禁用)这个 Refresh Token 。这样,黑客的Access Token一旦过期,就再也换不到新 Token 了。
-
双Token总结
-
Access Token :负责前线打仗。寿命短、权限高、天天在网络上跑,过期了也不心疼。
-
Refresh Token :负责后方补给。寿命长、不见光、只在关键时刻出来给前线"续命",并且随时可以被总部(后端)切断。
通过这种"长短结合"的策略,既保证了用户不需要天天登录(极佳的 UX),又确保了即便 Token 泄露,黑客也无法长期作恶(极高的安全性)。
以上就是有关JWT的令牌机制的简单介绍,希望对你有所帮助~~ 如有纰漏,感谢指出哦~~