一、JWT 是什么?
JWT(JSON Web Token)是一种轻量级的、自包含的令牌格式,用于在双方之间安全地传输 JSON 格式的信息。它的核心特点是:
- 自包含:令牌本身包含了所有必要的用户信息(如用户 ID、角色),无需后端查库就能验证身份;
- 可验证:通过数字签名保证信息未被篡改,可快速验证合法性;
- 跨平台:基于 JSON 和 Base64URL,兼容所有编程语言和系统。
二、JWT 的核心结构(三段式)
JWT 由 . 分隔的三部分组成,格式为:Header.Payload.Signature,每部分都是 Base64URL 编码(可逆,非加密)。
| 部分 | 作用 | 示例(Base64URL 解码后) |
|---|---|---|
| Header | 声明令牌类型和签名算法 | {"alg":"HS256","typ":"JWT"} |
| Payload | 存储业务数据(Claims),仅存非敏感信息 | {"userId":1001,"username":"test","exp":1710990400} |
| Signature | 签名(防篡改),用 Header 声明的算法 + 密钥对 Header+Payload 计算得到 | 二进制哈希值转 Base64URL 后的字符串 |
关键提醒:
- Payload 不要存密码、手机号等敏感信息(Base64URL 可解码);
- exp(过期时间)是必加的 Claim,避免令牌永久有效。
三、JWT中的Signature是如何生成的?
JWT 签名的核心是「哈希算法 + 密钥」,主流算法有两类:
| 算法类型 | 代表算法 | 特点 |
|---|---|---|
| 对称加密 | HS256(HMAC-SHA256) | 签发和验证用同一个密钥,简单易实现 |
| 非对称加密 | RS256(RSA-SHA256) | 签发用私钥,验证用公钥,更安全(推荐) |
我们以HS256(HMAC-SHA256)为例讲解。
HS256 全称是 HMAC-SHA256,是一种基于哈希的消息认证码算法,也是 JWT 中最常用的签名算法:
- HMAC:带密钥的哈希算法,核心是「哈希算法 + 密钥」,没有密钥就无法生成 / 验证正确的签名;
- SHA256:哈希算法,把任意长度的数据转换成 256 位(32 字节)的二进制哈希值,不可逆;
- 对称加密特性:签发(生成签名)和验证(校验签名)使用同一个密钥,这是 HS256 最核心的特点。

1. HS256 签发 Signature 的完整步骤
签发就是 “生成 JWT 签名” 的过程,核心是对「编码后的 Header + 编码后的 Payload」用密钥做 HMAC-SHA256 运算,具体分 5 步:
步骤 1:定义 Header 和 Payload(原始数据)
Header 固定声明算法类型,Payload 存非敏感业务数据:
// Header:声明算法和令牌类型 { "alg": "HS256", "typ": "JWT" } // Payload:业务数据(绝对不存密码!) { "userId": 1001, "username": "test", "exp": 1710990400 // 过期时间戳 }步骤 2:对 Header/Payload 做 Base64URL 编码
Base64URL 是 Base64 的 URL 安全变种(替换
+→-、/→_,去掉末尾=),目的是让数据能在 URL/HTTP 头中安全传输:
- Header 编码结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9- Payload 编码结果:
eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0步骤 3:拼接待签名的原始字符串
用
.拼接编码后的 Header 和 Payload,得到 “待签名字符串”:unsignedStr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0"步骤 4:用密钥对 unsignedStr 做 HMAC-SHA256 运算
这是生成签名的核心步骤,伪代码逻辑:
// 密钥必须是 256 位(32 字节)以上的随机字符串(安全基础!) secretKey = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2" // 核心运算:HMAC-SHA256(密钥, 待签名字符串) → 得到二进制签名 binarySignature = HMAC-SHA256(secretKey, unsignedStr)注意:这里的密钥是只存储在服务器端的,服务器端所有 JWT 的签发 / 验证共用同一个核心密钥(除非做了多密钥版本管理),这个密钥在服务器初始化时加载(比如从配置中心读取),默认不会频繁变更。
步骤 5:对二进制签名做 Base64URL 编码
把二进制的
binarySignature转换成 Base64URL 字符串,就是最终的 Signature:// 示例编码结果(实际是32字节二进制转的Base64URL) encodedSignature = "5f8k8kZ7a9s8d7f6g5h4j3k2l1m0n9b8v7c6x5s4d3f2g1h0j9k8l7m6n5b4v3c2x1z"Signature 无 “解码” 一说,它是不可逆哈希值,仅用于对比验证,不存储任何数据;
最终:拼接完整 JWT
JWT = encodedHeader + "." + encodedPayload + "." + encodedSignature // 示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0.5f8k8kZ7a9s8d7f6g5h4j3k2
2.HS256 验证 Signature 的完整步骤
验证是签发的逆过程,核心是 “重新计算签名并对比”,确保 JWT 未被篡改、签发合法,分 4 步:
步骤 1:拆分接收到的 JWT
把前端传来的 JWT 按
.拆分成三部分:plaintext
jwt = "header.payload.signature" encodedHeader = 第一部分 encodedPayload = 第二部分 receivedSignature = 第三部分步骤 2:重新拼接 unsignedStr
用拆分出的
encodedHeader和encodedPayload重新拼接:plaintext
unsignedStr = encodedHeader + "." + encodedPayload步骤 3:用相同密钥重新计算签名
和签发时用同一个密钥,对新的
unsignedStr做 HMAC-SHA256 运算,再转 Base64URL 编码:plaintext
// 同一密钥 secretKey = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2" // 重新计算 newBinarySignature = HMAC-SHA256(secretKey, unsignedStr) newEncodedSignature = Base64URL(newBinarySignature)步骤 4:对比签名
- 如果
newEncodedSignature == receivedSignature:验证通过(JWT 合法、未被篡改);- 如果不相等:验证失败(JWT 被篡改,或密钥错误,直接拒绝)。
注意:JWT验证时,不会进行密码的验证
- 密码仅在登录阶段验证身份,验证通过后就退场,不会出现在 JWT 的 Payload 或 Signature 中;
- Signature 是服务器密钥生成的 “防伪标签”,用于验证 JWT 合法性,和密码无任何关联,也无法 “解码” 出任何内容;
- JWT 验证的核心是 “令牌是否合法”,而非 “密码是否正确”—— 因为合法令牌本身就代表用户已通过密码验证。
四、为什么JWT验证时,不会进行密码的验证
密码既不会存储在 Payload 中,也绝不会出现在 Signature 里,当用户名和密码在数据库当中被搜索到,验证了用户的身份之后,服务器就会给浏览器签发JWT,之后整个 JWT 生命周期中都不会和密码有关联。
- 密码的作用范围:仅在用户登录阶段验证身份,验证通过后就彻底 “退场”,不会参与 JWT 的生成和验证;
- Signature 的作用:仅用于验证 “JWT 是否由服务器签发、是否被篡改”,和密码无关;
- JWT 验证的核心:验证的是 “令牌的合法性”,而非 “密码的正确性”—— 因为登录阶段已经验证过密码了。
密码验证成功,签发JWT流程如下。

五、JWT令牌的安全风险
1.JWT 的编码≠加密,拦截后能看到什么?
JWT 由 3 部分组成(Header.Payload.Signature),默认采用 Base64URL 编码(不是加密):
- Header:令牌类型、加密算法(如 HS256)→ 可解码;
- Payload:存储的自定义数据(如用户 ID、用户名、过期时间)→ 可解码;
- Signature:签名(防止篡改)→ 无法逆向解析。
黑客拦截后,只需把前两段(Header、Payload)单独拿出来做 Base64 解码,就能看到:
- Header 解码结果:
{"alg":"HS256","typ":"JWT"} - Payload 解码结果:
{"userId":1,"name":"admin","exp":1710990400}
👉 结论 1:如果 Payload 里直接存了明文密码,黑客解码后能直接拿到;但规范的 JWT 绝不会在 Payload 里存密码,只会存非敏感的用户标识(如 ID、用户名)。
👉 结论 2:即使 Payload 里没有密码,黑客能看到用户 ID / 用户名等信息,依然有信息泄露风险。
2.黑客拦截 token 后,能做什么?
- 冒充合法用户:黑客拿着有效的 token,直接在请求头 / Cookie 中携带,就能以你的身份访问系统(直到 token 过期);
- 信息泄露:解码 Payload 后获取用户 ID、用户名、角色等信息,为进一步攻击提供线索;
- 尝试篡改 token:比如把
userId:1改成userId:2(管理员),但签名验证会失败(除非黑客拿到你的签名密钥)。
六、黑客拦截 JWT 后直接发送给服务端,不就可以冒充用户了?
核心结论:这是 JWT 最大的风险(“令牌冒用”),但可以通过多层防护彻底规避。
首先承认:如果黑客能拿到有效的 JWT,并且直接用它发请求,在 token 过期前确实能冒充用户 —— 但这不是 JWT 的设计缺陷,而是传输 / 存储环节的安全问题,我们可以通过以下 5 层防护,让黑客 “拿不到” 或 “拿到也用不了”:
防护 1:传输层加密(最基础)—— 让黑客拿不到 JWT
JWT 只要通过 HTTP 明文传输,黑客就能通过抓包(Fiddler/Wireshark)拦截;必须强制用 HTTPS:
- HTTPS 会对整个请求(包括 Header/Cookie 中的 JWT)加密,黑客抓包只能拿到密文,无法解析出 JWT;
- 配合 Cookie 的
Secure属性:设置cookie.setSecure(true),让 JWT 仅在 HTTPS 下传输,HTTP 请求不会携带。
防护 2:限制 Cookie 访问权限 —— 让黑客偷不走 JWT
如果 JWT 存在 Cookie 中,必须设置以下属性:
Cookie cookie = new Cookie("token", jwt);
cookie.setHttpOnly(true); // 禁止前端JS读取(防XSS攻击偷取)
cookie.setSameSite("Strict"); // 禁止跨域请求携带(防CSRF攻击)
cookie.setSecure(true); // 仅HTTPS传输
cookie.setMaxAge(30 * 60); // 短过期时间(30分钟)
response.addCookie(cookie);
HttpOnly=true:黑客即使注入恶意 JS 脚本,也无法通过document.cookie读取 JWT;SameSite=Strict:黑客诱导用户点击恶意链接时,浏览器不会携带该 Cookie,无法冒用。
防护 3:缩短 token 过期时间 + 刷新令牌机制 —— 即使拿到也用不久
- 访问令牌(Access Token):有效期设为 30 分钟(短),用于接口访问;
- 刷新令牌(Refresh Token):有效期设为 7 天(长),仅用于获取新的 Access Token;
- 逻辑:Access Token 过期后,前端用 Refresh Token 换全新的 Access Token;如果 Refresh Token 也过期,用户必须重新登录。
- 效果:即使黑客拿到 Access Token,最多只能冒用 30 分钟,风险窗口极小。
防护 4:维护 token 黑名单 —— 让被盗的 token 失效
如果发现 token 泄露(比如用户反馈账号异常),后端立即将该 token 加入黑名单(存 Redis,过期时间和 token 一致):
防护 5:传输层加密,强制使用 HTTPS
Cookie 和请求头在 HTTP 协议下是明文传输,黑客可通过抓包(如 Fiddler、Wireshark)直接拦截;HTTPS 会对整个通信过程加密,杜绝抓包获取 token
HTTPS密文传输过程
前置理解:服务器会提前准备一个HTTPS 密钥对(公钥 + 私钥),这个密钥对默认是长期固定的,不会随服务器重启 / 请求变化,但也可以主动更新(只是更新频率极低)。为了方便,我们默认服务器的公钥和私钥保持不变。
- 客户端(浏览器)向服务器发起 HTTPS 请求,服务器返回公钥证书(包含服务器公钥,可公开);
- 客户端验证证书合法后,生成一个随机的临时会话密钥(比如 128 位随机字符串);
- 客户端用服务器的公钥加密这个临时会话密钥,发送给服务器;
- 服务器用自己的私钥解密,拿到这个临时会话密钥;
- 后续的所有请求 / 响应,都用这个临时会话密钥做对称加密(一次一密);
- 会话结束后,临时会话密钥立即销毁,下次请求重新生成。
防护 6:绝对不要在 JWT Payload 中存敏感信息
这是最基础的原则:
- ❌ 错误做法:
{"username":"admin","password":"123456","exp":xxx} - ✅ 正确做法:
{"userId":1001,"role":"user","exp":1710990400}(仅存非敏感标识)
七、代码实现
1. Java中与JWT技术相关的常见类

- Jwt接口和类都存在但日常几乎都不使用。Jwt接口是JJWT 库的顶层接口,定义了 JWT 的基础结构;Jwt类是不加密、无签名的JWT令牌。JWT(广义) = JWS(带签名) + JWE(加密) + 纯明文JWT(无签名/加密,不推荐)
- Jws是带数字签名的 JWT(也是最常用的 JWT 形式),99%的JWT令牌都是Jws的形式
- Jwe是加密的 JWT,不常见。
- Jwts是Java JJWT 库中提供的静态工具类(
io.jsonwebtoken.Jwts),是操作 JWT(主要是 JWS)的入口。封装了创建 JWT、解析 JWT、验证 JWT 的所有核心方法,相当于操作 JWT 的 “工具箱”。Jwts.claims()创建 Claims 载荷对象;Jwts.parserBuilder()创建解析器(用于解析 / 验证 JWS 令牌);Jwts.builder()创建 JwtBuilder 对象(用于生成 JWS 令牌) - JwtBuilder是JJWT 库中的接口,不能直接new,由
Jwts.builder()返回,用于一步步构建 JWS 令牌。利用setHeader,setClaims,signWith,toString等方法构建令牌。注意:JJWT 库设计了建造者模式,必须通过Jwts.builder()静态方法获取 JwtBuilder 实例(而非直接 new) - JwtParser是JJWT 库中的接口,不能直接new,由
Jwts.parseBuilder()返回,用于解析JWT令牌。利用setSigningKey,parseClaimsJws等方法构建令牌。 Jws<Claims>是 解析后的 JWS 令牌对象,封装了 JWS 格式 JWT 的三部分:Header(头)、Body(载荷,即 Claims)、Signature(签名);是JwtParser解析后的返回结果,代表 “验证通过的合法令牌”。常见方法有getHeader,getClaims,getSignature-
Claims是JWT 载荷,作为业务数据载体,存储 JWT 的所有载荷数据是
Jws<Claims>的核心内容,业务层几乎所有操作都围绕它展开。包括:标准字段:sub(用户名)、exp(过期时间)、iat(签发时间)、jti(令牌 ID)等;自定义字段:用户 ID、角色、部门等业务数据;
2. 代码
public class JwtUtils{
private static String privateKey = "123456";
public static String gernerateJWT(String username){
//JWT令牌构建
Map<String,Object> header = new HashMap<>();
header.put("alg","HMAC-SHA256");
Map<String,Object> claims = new HashMap<>();
claims.put("username",username);
String jwt = Jwts.builder()
.setHeader(header)
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, privateKey)
.setExpiration(new Date(System.currentTimeMillis() + 24*60*60*1000)) // 设置过期时间为24小时
.compact();
return jwt;
}
public static Claims parseJWT(String jwt){
//JWT令牌解析
Jws<Claims> claims = Jwts.parser().setSigningKey(privateKey).parseClaimsJws(jwt);
return claims.getBody();
}
}

1089

被折叠的 条评论
为什么被折叠?



