【技术文章1|JWT 深度解析:手写实现 vs JJWT 库 API 实战总结】
1.认证机制的演进:Cookie、Session 与 JWT 的简单区分
在Web应用中,身份验证和状态管理是必不可少的。常见的三种方案是:Cookie、Session和Token(其中JWT是Token的一种实现)。下面对它们进行简单区分。
Cookie
定义
Cookie是存储在浏览器中的一小块数据,用于保存一些信息(如认证信息、用户偏好等),并且可以随着HTTP请求发送到服务器
工作原理
- 客户端请求登录,发送用户名和密码。
- 服务器认证成功后,通过响应头将Cookie返回给浏览器,浏览器会自动保存这个Cookie。
- 后续每次请求时,浏览器都会将该Cookie自动发送给服务器。
缺点
- 安全性差:Cookie可以被篡改、盗用,特别是当存储敏感信息时。
- 大小有限制:Cookie的数据量限制较小(大约4KB)。
- 跨域问题:默认情况下,Cookie只能在同一域下访问,如果有跨域请求时需要特别处理。
session
定义
Session是由服务器创建的会话数据存储机制,通常在服务端存储用户的状态信息,如用户ID、登录信息等。用户每次请求都会携带SessionID,服务器通过这个ID来识别用户的状态。
工作原理
- 客户端请求登录,发送用户名和密码。
- 服务器验证用户信息后,如果认证成功,创建一个SessionID(无规律)和回话结束时间等,并将其保存在服务器端。然后将SessionID返回给浏览器,通常通过Cookie存储。并把会话时间设置为这个cookie的有效期
- 浏览器将该SessionID保存到Cookie中,后续每次请求时都会将SessionID随请求发送给服务器。
- 服务器通过SessionID在自己的会话存储中查找对应的用户信息。
- 直到有效期结束,浏览器会自行删除这个cookie(在Cookies失效之后,用户就得重新输入用户名密码了)
缺点
-
存储压力:服务器需要存储大量的Session数据,随着用户数量的增加,可能会消耗大量内存。
-
扩展性差:如果应用部署在多个服务器上,可能面临Session共享问题,需要采用分布式存储(如Redis)来解决。
-
依赖服务器状态:服务器的数据库存储sessionId,服务器宕机,这样就影响服务器获取sessionId
JWT
说jwt之前,要先说一下token
token是什么
Token(令牌) 用于身份验证和授权的机制,通常是一个由服务端生成的字符串(如API Key、OAuth Token等),它携带了用户或客户端的身份信息,用于后续请求时验证用户身份和访问权限。Token 的主要作用是证明客户端已经通过身份验证,可以访问某些资源。
可以简单理解为:Token 是一种概念/思想,而 JWT 是 Token 的一个具体实现标准
JWT的定义
JWT(JSON Web Token)是一种是一种特殊类型的 Token,是一种轻量级的身份验证机制,它采用一种标准格式来传输信息,包含三部分:头部(Header)、载荷(Payload)、签名(Signature)。JWT的存储和验证机制不同于传统的Session,它是无状态的,用户的认证信息通过JWT保存在客户端。
工作原理
- 客户端请求登录,发送用户名和密码。
- 服务器(服务器保存密钥secret)验证信息正确后生成JWT。JWT包含三部分:
- Header:通常包括签名算法(如HS256)和Token类型(JWT)。
- Payload:包含具体的用户信息,如用户ID、权限等。
- Signature:由Header和Payload使用服务器的密钥进行加密计算得到,确保数据未被篡改。
- 将Signature与header、payload组合成完整jwt,服务器将JWT返回给浏览器,客户端将JWT保存在Cookie或LocalStorage中
- 每次请求时,客户端都会携带这个JWT,服务器通过密钥验证JWT的有效性。
优点
JWT 相比 Session/Cookie 的核心优点如下:
- 无状态与高扩展性:服务器不需要在内存或数据库里存 Session,减轻了存储压力,也不存在多台服务器之间“Session 不同步”的问题,非常适合分布式微服务。
- 性能高效: 自包含用户信息,服务器通过签名校验即可解析,减少了数据库查询次数。
- 更安全(防 CSRF): 不依赖 Cookie 自动发送机制,从根源上规避了 CSRF 攻击。
- 跨平台通用: 字符串格式不依赖浏览器,完美适配移动端 App 和小程序。
2.深度拆解:JWT 的构造与验证原理
2.1jwt的三部分组成
jwt是由三部分组成
- header:算法和token类型
- payload:数据载体
- signature(秘钥)
标准格式为 Header.Payload.Signature。
1、Header 头部
Header 部分通常包含两部分信息,是一个JSON 对象, 描述JWT的元数据
-
alg:指定签名使用的算法,常见的有
HS256、RS256等。HS256是 HMAC SHA-256 的一种对称加密算法。 -
typ:指定这个令牌的类型,通常为
JWT。
例子:
{
"alg": "HS256",
"typ": "JWT"
}
2、payload 负载
它包含了与身份认证相关的数据。它是一个JSON对象,包含了许多字段(也叫做声明)。Payload 中的声明信息可以是自定义的,或者是JWT标准中预定义的字段。
- JWT 规范预定义的字段如下,旨在提供一些通用的字段,帮助你在 JWT 中传递有用的信息:
- iss(Issuer):签发者,指明谁生成了这个JWT。
- sub(Subject):JWT的主体,通常是用户的唯一标识符(例如,用户ID)。
- aud(Audience):受众,通常是JWT的接收者。
- exp(Expiration Time):过期时间,JWT的有效期,过期后无效。
- nbf(Not Before):指定JWT的有效时间段,
nbf指定JWT在某个时间之前不可用。 - iat(Issued At):JWT的签发时间,表示JWT被创建的时间。
- jti(JWT ID):JWT的唯一标识符,避免Token重放攻击。
例子:
{
"sub": "7711",
"name": "irving",
"admin": true
}
注意:JWT 的 Payload 部分不进行加密,任何人都可以解码并查看里面的数据。因此 不要在 Payload 中存储敏感信息,例如用户的密码、信用卡号等。
3、signature 签名
Signature 部分的作用是确保 JWT 的完整性,并且验证 JWT 是否在传输过程中被篡改。它是由头部、载荷和密钥共同生成的
步骤如下:
- 将 Header 和 Payload(这两部分都是 JSON 对象)转换为字符串,并进行 Base64Url 编码。
- 使用 Header 中指定的**签名算法(如 HMAC SHA256 )**和一个 密钥(secret key)对编码后的 Header 和 Payload 进行签名。(这个密钥只有服务器才知道,不能泄露给客户端 )
例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
也就是signature等于上面公式算出来的
总结
JWT 由三部分组成:Header、Payload 和 Signature,这三部分通过 . 分隔,形成一个完整的 JWT 格式:
-
Header:
header经过 Base64Url 编码。 -
Payload:
payload经过 Base64Url 编码。 -
Signature:通过 HMACSHA256 算法(或者其他算法)生成签名并进行 Base64Url 编码。
2.2服务器端验证JWT的详细过程
-
客户端在每次请求时,会将JWT(通常存储在Cookie或LocalStorage中)携带在HTTP请求头中,通常是放在
Authorization头部 -
服务器接收到请求后,从
Authorization头部提取出JWT。 -
分解JWT:JWT被编码为三部分(Header、Payload 和 Signature),每部分之间通过
.(点)进行分隔。- 服务器首先会将JWT分解为三个部分:
- Header: 记录了JWT的签名算法(如
HS256),以及类型(JWT)。 - Payload: 存储了载荷数据(如用户ID、过期时间等)。
- Signature: 由Header和Payload以及服务器密钥通过签名算法计算出来,确保数据没有被篡改。
- Header: 记录了JWT的签名算法(如
- 服务器首先会将JWT分解为三个部分:
-
验证Signature:
- 获取签名算法:首先,服务器根据JWT的Header部分中的
alg字段,知道使用的签名算法(如HS256、RS256等)。 - 重新计算签名:服务器根据请求中JWT的Header和Payload部分,根据签名算法以及密钥,重新计算签名。
- 将 Header 和 Payload 部分进行 Base64Url编码。
- 使用签名算法(如 HMAC SHA256)和密钥(或者公钥)对这两部分进行签名计算,得到服务器端的签名。
- 比较签名:将服务器重新计算的签名与JWT中传过来的Signature部分进行对比
- 果计算出来的签名和JWT中的Signature一致,说明JWT有效,没有被篡改。
- 如果签名不一致,说明JWT可能被篡改,验证失败,服务器会拒绝请求。
- 获取签名算法:首先,服务器根据JWT的Header部分中的
-
验证Payload中的信息
除了验证签名,服务器还需要对 Payload 中的一些字段进行额外的验证,通常包括:
exp(过期时间):JWT中通常包含一个exp字段,表示Token的过期时间,服务器可以检查当前时间是否超过该过期时间。如果Token已经过期,服务器会拒绝请求。iat(签发时间):可以检查Token是否在合理的时间内签发。aud(Audience,受众):可以验证JWT是否面向当前服务器,确保Token是为该服务器签发的。sub(Subject,主题):可以验证JWT的主题是否是预期的用户或客户端。iss(Issuer,签发者):可以验证Token是否由可信的签发者签发。
-
允许或拒绝请求
- 如果JWT的签名有效且Payload通过验证,服务器就可以通过JWT中的信息进行业务处理(如获取用户数据、执行操作等)。
- 如果JWT验证失败(例如签名不匹配、Token过期等),服务器会返回
401 Unauthorized错误,要求客户端重新登录。
现在,我们已经知道了jwt的原理,让我们来看看,在java当中我们可以怎么实现这种功能!
3.Java 实战:从底层手写到 API 调用
我们先通过底层代码模拟生成过程,再引入工业级的 JJWT 库。
3.1手写创建JWT和跟解析JWT两个方法
创建JWT
代码
public static String generateStandardJwt() {
// 1. Header
JSONObject header = new JSONObject();
header.put("alg", "HS256");
header.put("typ", "JWT");
// 2. Payload
JSONObject payload = new JSONObject();
payload.put("userId", "24");
payload.put("userName", "kobe");
// 3. 使用 Base64URL 编码
String jwtHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes(StandardCharsets.UTF_8));
String jwtPayLoad = Base64.encodeBase64URLSafeString(payload.toJSONString().getBytes(StandardCharsets.UTF_8));
// 4. 密钥
String secret = "miyao";
// 5.签名是针对前两部分拼接后的字节流进行 HMAC 计算
String content = jwtHeader + "." + jwtPayLoad;
byte[] signBytes = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmac(content);
// 6. 签名部分做 Base64URL 编码
String sign = Base64.encodeBase64URLSafeString(signBytes);
// 7. 组合最终 Token
String jwt = jwtHeader + "." + jwtPayLoad + "." + sign;
return jwt;
}

测试一下
测试网站:https://www.jwt.io/
当我们输入正确的秘钥的时候,发现是成功的

且我们只能对Header跟PayLoad部分进行解码,对签名部分我们无法进行简单的解码:

解析JWT
public static void parseJwt(String jwt){
//秘钥
String secret = "miyao";
// 1. 拆分
String[] parts = jwt.split(".");
if (parts.length != 3) {
throw new RuntimeException("JWT 格式非法");
}
String headerEncoded = parts[0];
String payloadEncoded = parts[1];
String signatureProvided = parts[2];
// 2. 校验签名 (验证数据是否被篡改)
String content = headerEncoded + "." + payloadEncoded;
// 同样使用 HS256 算法和你的密钥重新算一遍签名
byte[] signBytes = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmac(content);
String signatureExpected = Base64.encodeBase64URLSafeString(signBytes);
if (signatureExpected.equals(signatureProvided)) {
System.out.println("签名验证通过!");
// 3. 解码 Payload 获取数据
byte[] decodedPayload = Base64.decodeBase64(payloadEncoded);
String jsonPayload = new String(decodedPayload, StandardCharsets.UTF_8);
System.out.println("解析出的数据: " + jsonPayload);
// 如果想转回 JSONObject
JSONObject jsonObject = JSONObject.parseObject(jsonPayload);
System.out.println("用户ID: " + jsonObject.getString("userId"));
} else {
System.err.println(" 签名验证失败!数据可能被篡改。");
}
}
测试一下

假如我们修改jwt,就会解析失败!!!

3.2使用简单API创建和解析JWT
导入依赖
0.11.x 新写法
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.5version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.5version>
<scope>runtimescope>
dependency>
JWT工具类
package com.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
public class JwtUtil {
private static final SecretKey KEY =
Keys.hmacShaKeyFor(
"miyao-miayo-miayo-miayo-12345678".getBytes(StandardCharsets.UTF_8)
);
public static String generateJwt() {
// 2. 生成 JWT
String jwt = Jwts.builder()
//设置 Header(可选),alg 不用你写,signWith(HS256) 会自动写进去
.setHeaderParam("typ", "JWT")
//设置 Payload(核心数据)或者传递map直接.setClaims(map)
.claim("userId", "24")
.claim("userName", "kobe")
//设置签名
.signWith(KEY, SignatureAlgorithm.HS256)
//生成最终 Token,拼成最终字符串:xxxxx.yyyyy.zzzzz
.compact();
return jwt;
}
public static void parseJwt(String jwt) {
try {
//构建解析器,设置秘钥 KEY
Claims claims = Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
/*
parseClaimsJws(jwt)是核心校验方法
校验 JWT 结构(3 段)
校验签名(是否被篡改)
校验算法是否匹配(HS256)
*/
.parseClaimsJws(jwt)
//取出 Payload
.getBody();
System.out.println("签名验证通过!");
System.out.println("userId = " + claims.get("userId"));
System.out.println("userName = " + claims.get("userName"));
} catch (Exception e) {
System.err.println("JWT 校验失败:" + e.getMessage());
}
}
}
测试


补充一个旧版的写法
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
package com.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
public class JwtUtil {
private static final String SECRET =
"miyao-miayo-miayo-miayo-12345678";
public static String generateJwt() {
String jwt = Jwts.builder()
// Header(alg 会自动设置)
.setHeaderParam("typ", "JWT")
// Payload
.claim("userId", "24")
.claim("userName", "kobe")
//设置签名使用的签名算法和签名使用的秘钥( 旧版 signWith:算法在前,秘钥在后)
.signWith(SignatureAlgorithm.HS256, SECRET.getBytes(StandardCharsets.UTF_8))
.compact();
return jwt;
}
public static void parseJwt(String jwt) {
try {
Claims claims = Jwts.parser()
// 旧版 parser(),不是 parserBuilder()
.setSigningKey(SECRET.getBytes(StandardCharsets.UTF_8))// 设置签名的秘钥
.parseClaimsJws(jwt) // 设置需要解析的jwt
.getBody();//取出 Payload
System.out.println("签名验证通过!");
System.out.println("userId = " + claims.get("userId"));
System.out.println("userName = " + claims.get("userName"));
} catch (Exception e) {
System.err.println("JWT 校验失败:" + e.getMessage());
}
}
}
3.3项目实践中的登录流程
在实际项目中,登录功能通常可以分为两步:
- 登录时生成Token
- 用户提交用户名/密码。
- 服务器验证成功后,生成JWT并返回给客户端。
- 客户端存储JWT(Cookie或LocalStorage)。
- 后续请求验证Token
- 通过拦截器或过滤器获取请求头中的JWT。
- 解析并验证JWT,若通过则允许访问接口,否则返回401。










