JWT安全最佳实践:Token认证原理、常见风险与防护指南
在现代Web应用开发中,用户登录、接口认证和权限控制是几乎所有系统都需要解决的问题。
传统Web应用经常使用Session保存用户登录状态,而在前后端分离、移动端应用、REST API以及微服务架构中,Token认证逐渐成为一种常见方案。
JWT(JSON Web Token)就是其中应用非常广泛的一种Token格式。
JWT可以将用户身份、Token有效期以及其他声明信息封装到一个Token中,客户端在访问需要认证的接口时携带这个Token,服务器验证Token后确定用户身份。
常见应用场景包括:
- 用户登录认证
- 前后端分离项目
- REST API认证
- 移动端App登录
- 微服务身份认证
- 单点登录
- API访问授权
- 服务之间的身份传递
不过,JWT本身并不等于“安全认证”。
如果没有正确处理JWT的签名算法、密钥、Token有效期、存储方式以及权限控制,就可能出现Token泄露、身份冒用、越权访问甚至整个认证系统被绕过等问题。
因此,理解JWT的工作原理以及正确的安全实践非常重要。
本文将系统介绍JWT是什么、JWT的组成结构、认证流程、常见算法、Access Token和Refresh Token、Token存储方式,以及生产环境中应该注意的安全问题。
一、什么是JWT?
JWT全称:
JSON Web Token
JWT是一种开放标准,用于在不同系统之间以JSON对象的形式传递声明信息。
JWT最大的特点是结构紧凑,并且可以通过数字签名验证Token是否被篡改。
一个JWT通常类似于:
xxxxx.yyyyy.zzzzz
也就是:
Header.Payload.Signature
三个部分使用英文句号.进行连接。
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMDAwMSIsInJvbGUiOiJ1c2VyIn0
.
xxxxxxxxxxxxxxxxxxxxxxxx
其中:
- Header:描述Token类型和签名算法
- Payload:保存用户相关声明
- Signature:验证Token完整性和真实性
二、JWT有什么作用?
JWT最常见的用途就是身份认证。
例如用户登录:
用户
↓
输入账号密码
↓
服务器验证账号密码
↓
验证成功
↓
生成JWT
↓
返回客户端
之后客户端访问:
GET /api/user/profile
可以携带:
Authorization: Bearer <JWT>
服务器收到请求以后:
获取JWT
↓
验证Token
↓
确认用户身份
↓
检查权限
↓
执行接口
这样服务器就可以知道:
这个请求是谁发起的。
三、JWT的主要特点
JWT之所以被广泛使用,主要有以下几个特点。
1. 结构简单
JWT本质上是一段字符串:
Header.Payload.Signature
非常适合通过HTTP请求传输。
2. 跨平台
JWT并不依赖某一种编程语言。
Java、JavaScript、Python、Go、PHP、C#等语言都可以生成和解析JWT。
例如:
Java服务
↓
生成JWT
↓
Vue前端
↓
携带JWT
↓
Node.js服务
不同技术栈之间也可以使用JWT进行身份认证。
3. 适合API认证
JWT经常用于:
前端
↓
API Gateway
↓
后端服务
尤其适合前后端分离项目。
4. 可以进行数字签名
JWT可以使用密钥生成Signature。
如果Token内容被修改,服务器重新计算签名时就会发现不匹配。
因此JWT可以用于验证:
- Token完整性
- Token来源
- Token是否被篡改
四、JWT由哪几个部分组成?
JWT由三个部分组成:
Header.Payload.Signature
例如:
xxxxx
.
yyyyy
.
zzzzz
分别对应:
Header
Payload
Signature
下面分别介绍。
五、JWT Header是什么?
Header是JWT的第一部分。
常见Header:
{
"alg": "HS256",
"typ": "JWT"
}
其中:
alg
表示签名算法。
例如:
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256
而:
typ
表示Token类型。
通常:
JWT
六、JWT Payload是什么?
Payload是JWT的第二部分。
它主要用于保存Claims,也就是声明信息。
例如:
{
"sub": "10001",
"username": "tom",
"role": "user",
"iat": 1720000000,
"exp": 1720003600
}
这里包含:
sub
用户标识
username
用户名
role
角色
iat
Token签发时间
exp
Token过期时间
七、JWT常见Claims
JWT定义了一些常见的标准Claims。
| Claim | 名称 | 作用 |
|---|---|---|
| iss | Issuer | Token签发者 |
| sub | Subject | Token主体 |
| aud | Audience | Token接收方 |
| exp | Expiration Time | Token过期时间 |
| nbf | Not Before | Token生效时间 |
| iat | Issued At | Token签发时间 |
| jti | JWT ID | Token唯一标识 |
例如:
{
"iss": "auth.example.com",
"sub": "10001",
"aud": "api-server",
"iat": 1720000000,
"exp": 1720003600
}
八、JWT Payload是不是加密的?
这是JWT使用过程中非常重要的一点。
普通JWT的Payload通常不是加密数据。
JWT主要是经过Base64URL编码,而不是简单意义上的加密。
因此拿到JWT以后,通常可以解析:
Header
Payload
例如:
{
"sub": "10001",
"role": "user"
}
就可能被读取。
因此不要把以下信息直接放入Payload:
- 用户密码
- 数据库密码
- API密钥
- 私钥
- 银行卡信息
- 身份证号码
- 其他敏感信息
JWT签名可以防止数据被非法修改,但不等于隐藏数据内容。
九、JWT Signature是什么?
Signature是JWT第三部分。
它主要用于验证:
JWT是否被修改,以及是否由可信的一方签发。
以HS256为例,可以简单理解为:
HMACSHA256(
Base64Url(Header)
+
"."
+
Base64Url(Payload),
Secret
)
最终得到Signature。
完整过程:
Header
+
Payload
+
Secret
↓
签名算法
↓
Signature
最终组成:
Header.Payload.Signature
十、JWT为什么可以防止Token被篡改?
假设服务器签发:
{
"sub": "10001",
"role": "user"
}
攻击者修改成:
{
"sub": "10001",
"role": "admin"
}
由于Payload发生了变化:
原始Payload
↓
原始Signature
修改Payload
↓
重新计算Signature
↓
与原Signature不一致
服务器验证失败以后,就应该拒绝这个Token。
所以:
Signature是JWT安全体系中的核心机制。
十一、JWT完整认证流程
一个典型的JWT认证过程如下:
用户
↓
登录页面
↓
输入用户名和密码
↓
POST /login
↓
服务器验证账号密码
↓
验证成功
↓
生成Access Token
↓
生成Refresh Token
↓
返回客户端
客户端之后访问API:
客户端
↓
携带Access Token
↓
Authorization: Bearer JWT
↓
服务器
↓
验证JWT
↓
获取用户身份
↓
检查权限
↓
返回数据
当Access Token过期:
Access Token过期
↓
Refresh Token
↓
请求刷新接口
↓
验证Refresh Token
↓
生成新的Access Token
↓
继续访问API
十二、Authorization Bearer是什么?
JWT经常通过HTTP Authorization请求头传递。
例如:
GET /api/user/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
其中:
Bearer
表示客户端使用Bearer Token进行身份认证。
服务器通常需要从Authorization中提取:
Bearer
+
Token
然后验证Token。
十三、为什么生产环境必须使用HTTPS?
JWT本身是一种认证凭证。
如果攻击者能够在网络传输过程中获取Token,就可能冒充用户。
例如:
用户
↓
HTTP
↓
JWT
↓
服务器
如果网络环境不安全,Token可能被窃取。
因此生产环境应该:
用户
↓
HTTPS
↓
JWT
↓
服务器
HTTPS可以保护客户端和服务器之间的通信内容。
生产环境建议:
- 全站使用HTTPS
- HTTP自动跳转HTTPS
- 开启安全TLS配置
- 不通过HTTP发送JWT
- 不在不安全网络中传输敏感认证信息
十四、JWT不能放在URL中
不推荐:
https://example.com/api/user?token=xxxxx
因为URL可能进入:
- 浏览器历史记录
- Web服务器日志
- Nginx日志
- CDN日志
- 代理日志
- 监控系统
- Referer信息
因此更推荐:
Authorization: Bearer <JWT>
十五、Access Token是什么?
Access Token是访问API资源的凭证。
例如:
POST /api/order
Authorization: Bearer <Access Token>
服务器验证Access Token以后允许用户访问接口。
Access Token通常具有:
- 较短生命周期
- 明确的权限范围
- 较小的Payload
- 较低的泄露影响范围
十六、为什么Access Token应该设置较短有效期?
假设Access Token永久有效:
Token泄露
↓
攻击者获得Token
↓
长期使用
↓
用户身份长期被冒用
如果Token只有较短生命周期:
Token泄露
↓
攻击者获得Token
↓
Token到期
↓
无法继续使用
因此Access Token通常不建议设置成永久有效。
具体有效时间需要根据业务安全等级决定。
十七、Refresh Token是什么?
Refresh Token用于获取新的Access Token。
例如:
Access Token
有效期较短
Refresh Token
有效期较长
当Access Token过期:
客户端
↓
Refresh Token
↓
刷新接口
↓
认证服务器
↓
验证Refresh Token
↓
返回新的Access Token
这样就不需要让Access Token长期有效。
十八、Access Token和Refresh Token区别
| 项目 | Access Token | Refresh Token |
|---|---|---|
| 用途 | 访问API | 获取新Access Token |
| 有效期 | 较短 | 较长 |
| 使用频率 | 高 | 较低 |
| 泄露影响 | 较短期 | 风险更大 |
| 存储要求 | 较高 | 非常高 |
| 是否用于普通API | 是 | 否 |
可以简单理解为:
Access Token
=
短期通行证
Refresh Token
=
长期续期凭证
十九、Refresh Token Rotation
对于安全要求较高的系统,可以使用Refresh Token Rotation。
流程:
旧Refresh Token
↓
刷新请求
↓
验证成功
↓
生成新的Access Token
+
新的Refresh Token
↓
旧Refresh Token失效
这样可以降低Refresh Token长期重复使用带来的风险。
如果系统发现一个已经失效的Refresh Token再次出现,还可以将其视为潜在的Token盗用信号。
二十、JWT最常见的安全问题
JWT常见安全风险包括:
| 安全问题 | 可能造成的后果 |
|---|---|
| Secret泄露 | Token可能被伪造 |
| Private Key泄露 | 攻击者可能签发Token |
| Token泄露 | 用户身份被冒用 |
| Token永久有效 | 泄露后长期有效 |
| 不验证Signature | 可能接受伪造Token |
| 不验证exp | 过期Token继续使用 |
| Payload保存敏感数据 | 敏感信息泄露 |
| Token放URL | 容易进入日志 |
| 完整Token写日志 | 凭证泄露 |
| 权限检查不足 | 越权访问 |
| XSS | Token可能被窃取 |
| CSRF | Cookie认证可能被滥用 |
二十一、JWT必须验证Signature
这是JWT开发中最重要的原则之一。
错误方式:
收到JWT
↓
解析Payload
↓
读取userId
↓
直接相信
攻击者可能修改:
{
"userId": "10001",
"role": "admin"
}
如果服务器没有验证Signature,就可能产生严重的权限绕过问题。
正确流程:
收到JWT
↓
验证Token格式
↓
验证Signature
↓
验证算法
↓
验证exp
↓
验证iss
↓
验证aud
↓
获取身份
↓
检查权限
↓
执行请求
二十二、不要相信JWT中的role
例如Token中存在:
{
"sub": "10001",
"role": "admin"
}
服务器不能简单地认为:
role = admin
就一定允许访问管理员接口。
应该:
验证JWT
↓
确定用户身份
↓
获取用户权限
↓
检查接口权限
↓
允许或者拒绝
对于重要权限,应该结合服务端权限数据进行判断。
二十三、JWT与权限控制不是一回事
JWT主要解决:
你是谁?
权限系统解决:
你可以做什么?
例如:
Authentication
身份认证
↓
用户是谁?
Authorization
权限控制
↓
用户可以访问什么?
因此:
JWT验证成功
不代表:
所有接口都可以访问
二十四、什么是水平越权?
假设:
用户A
userId = 10001
用户B
userId = 10002
用户A请求:
GET /api/user/10002
如果服务器仅仅验证:
JWT有效
就返回用户B的数据,就产生了水平越权。
正确方式应该进一步验证:
当前用户
+
目标资源
+
资源所属关系
只有满足业务权限以后才能访问。
二十五、什么是垂直越权?
例如:
普通用户
尝试访问:
/admin/users
服务器不能因为JWT有效就允许访问。
应该检查:
用户身份
+
用户角色
+
接口权限
如果权限不足:
403 Forbidden
二十六、JWT Secret为什么非常重要?
HS256等对称签名算法依赖Secret。
例如:
Secret
↓
签名JWT
服务器:
Secret
↓
验证JWT
如果Secret泄露,攻击者可能利用Secret生成合法Token。
因此Secret必须受到严格保护。
二十七、JWT Secret不要写死在代码里
不推荐:
const secret = "123456";
也不推荐:
jwt:
secret: my-production-secret
然后直接提交到Git。
推荐:
环境变量
+
Secret管理系统
+
安全配置中心
例如:
JWT_SECRET
应用启动时读取。
二十八、不要把Secret提交到Git
错误流程:
Secret
↓
写入application.yml
↓
git add
↓
git commit
↓
git push
即使之后删除文件,Git历史中可能仍然存在。
如果生产Secret已经泄露:
立即生成新Secret
↓
停止使用旧Secret
↓
让旧Token失效
↓
重新签发Token
二十九、JWT应该使用什么算法?
常见算法:
| 算法 | 类型 | 特点 |
|---|---|---|
| HS256 | HMAC | 对称密钥 |
| HS384 | HMAC | 对称密钥 |
| HS512 | HMAC | 对称密钥 |
| RS256 | RSA | 非对称密钥 |
| RS384 | RSA | 非对称密钥 |
| RS512 | RSA | 非对称密钥 |
| ES256 | ECDSA | 非对称密钥 |
三十、HS256和RS256区别
HS256
HS256使用同一个Secret:
Secret
↓
签名
↓
JWT
↓
Secret
↓
验证
优点:
- 实现简单
- 性能较好
- 配置方便
适合:
- 单体应用
- 简单API
- 服务数量较少的系统
缺点:
所有验证JWT的服务都需要知道Secret。
RS256
RS256使用:
Private Key
+
Public Key
认证服务:
Private Key
↓
签名JWT
业务服务:
Public Key
↓
验证JWT
这样业务服务不需要拥有签发Token的私钥。
比较适合:
- 微服务
- 统一认证中心
- API Gateway
- 单点登录
- 大型系统
三十一、为什么微服务更适合非对称签名?
假设有:
认证服务
用户服务
订单服务
商品服务
支付服务
如果使用HS256:
认证服务
↓
Secret
↓
所有业务服务都需要Secret
一旦某个业务服务泄露Secret,风险可能扩散到整个系统。
使用RS256:
认证服务
↓
Private Key
↓
签发JWT
用户服务 → Public Key
订单服务 → Public Key
商品服务 → Public Key
支付服务 → Public Key
业务服务只能验证Token,而不能利用公钥签发新的合法Token。
因此对于统一认证中心和微服务架构,非对称签名通常更容易进行密钥隔离。
三十二、不要盲目信任alg
JWT Header中存在:
{
"alg": "RS256"
}
服务器不应该完全根据客户端提供的alg决定验证方式。
应该在服务端明确配置:
允许的算法
↓
RS256
收到JWT:
Token alg
↓
是否符合服务端策略
↓
验证
这样可以避免因算法处理不当而产生安全漏洞。
三十三、JWT应该验证哪些Claims?
生产环境通常需要根据业务验证:
exp
iss
aud
nbf
其中最重要的包括:
exp
判断Token是否过期。
当前时间 > exp
↓
Token失效
iss
确认Token是不是由可信认证中心签发。
aud
确认Token是不是发给当前服务使用。
nbf
确认Token是否已经达到生效时间。
三十四、JWT Token存储在哪里?
前端常见的存储方式:
- localStorage
- sessionStorage
- Cookie
- 内存
不同方式有不同安全特点。
没有一种方式可以脱离整体架构直接判断为“绝对安全”。
需要结合:
- XSS
- CSRF
- 跨域
- 前端框架
- API架构
- Token生命周期
进行选择。
三十五、localStorage存JWT安全吗?
例如:
localStorage.setItem("token", jwt);
使用方便,但是存在一个重要风险:
如果网站发生XSS攻击,恶意JavaScript可能读取localStorage中的Token。
攻击过程可能类似:
XSS
↓
执行恶意JavaScript
↓
读取localStorage
↓
获取JWT
↓
冒充用户
因此使用localStorage保存JWT时,必须特别重视XSS防护。
三十六、HttpOnly Cookie是什么?
如果通过Cookie保存认证信息,可以设置:
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Lax
其中:
HttpOnly
可以阻止普通JavaScript直接读取Cookie。
Secure
要求Cookie通过HTTPS发送。
SameSite
可以帮助降低部分跨站请求风险。
但是:
HttpOnly并不意味着网站完全安全。
如果使用Cookie进行身份认证,仍然需要正确设计CSRF防护。
三十七、JWT与XSS
XSS攻击可能让攻击者在用户浏览器中执行恶意JavaScript。
如果JWT可以被JavaScript直接读取:
XSS
↓
读取Token
↓
发送Token
↓
攻击者获得身份
因此应该:
- 对用户输入进行正确处理
- 避免不必要的HTML拼接
- 对富文本进行过滤
- 使用CSP
- 控制第三方脚本
- 及时更新依赖
- 避免不必要的innerHTML
三十八、JWT与CSRF
如果JWT放在Cookie中,由于浏览器可能自动携带Cookie,就需要考虑CSRF。
攻击者可能诱导用户访问恶意网站:
恶意网站
↓
发送请求
↓
浏览器自动携带Cookie
↓
目标服务器
因此Cookie认证方案应该结合:
- SameSite
- CSRF Token
- Origin检查
- Referer检查
- 请求来源控制
进行防护。
三十九、JWT注销为什么比较复杂?
Session模式下:
服务器
↓
Session
用户退出:
删除Session
↓
立即失效
JWT通常是:
JWT
↓
客户端持有
↓
服务器验证签名
如果服务器完全不保存Token状态,那么已经签发的Token可能在过期之前继续有效。
因此JWT注销需要额外设计。
四十、JWT如何实现退出登录?
常见方案包括:
方案一:短Token
让Access Token有效时间比较短。
Access Token
↓
较短有效期
用户退出后,Refresh Token失效。
方案二:Refresh Token撤销
服务器保存Refresh Token状态。
退出:
Refresh Token
↓
撤销
之后不能继续刷新Access Token。
方案三:Token黑名单
服务器保存已经撤销的Token或Token ID。
JWT
↓
jti
↓
Blacklist
验证时检查:
Token有效
+
Token不在Blacklist
方案四:Token版本号
用户数据保存:
tokenVersion = 1
JWT中:
tokenVersion = 1
强制退出所有设备后:
tokenVersion = 2
旧Token:
1 != 2
因此失效。
四十一、不要把完整JWT记录到日志
开发阶段经常会:
console.log(token);
生产环境不推荐。
因为日志可能进入:
- Nginx
- ELK
- Loki
- 云日志服务
- 监控平台
- 第三方日志系统
如果完整Token进入日志,攻击者获取日志以后可能直接获得用户身份凭证。
推荐:
不记录完整JWT
必要时只记录:
Token前几位
Token ID
用户ID
请求ID
并进行脱敏。
四十二、JWT Payload应该保持多大?
JWT会随着请求不断传输。
如果Payload保存大量数据:
用户信息
+
菜单
+
权限
+
订单
+
配置
+
其他业务数据
会导致JWT越来越大。
推荐只保存认证所需要的最小信息:
{
"sub": "10001",
"role": "user",
"iat": 1720000000,
"exp": 1720003600
}
详细业务数据应该通过API获取。
四十三、JWT不要保存密码
错误:
{
"username": "tom",
"password": "123456"
}
绝对不应该这样设计。
用户密码应该在服务器端使用安全的密码哈希算法进行处理。
常见选择:
- Argon2
- bcrypt
- scrypt
JWT中只保存用户身份标识等必要信息。
四十四、JWT与密码哈希有什么区别?
JWT和密码哈希解决的是不同问题。
| 技术 | 主要用途 |
|---|---|
| JWT | 用户身份认证和Token传递 |
| Hash | 数据摘要 |
| bcrypt | 密码哈希 |
| Argon2 | 密码哈希 |
| SHA-256 | 通用哈希 |
| HTTPS | 数据传输加密 |
例如用户登录:
用户名
+
密码
↓
服务器验证密码哈希
↓
登录成功
↓
生成JWT
所以:
JWT不是密码存储技术。
四十五、JWT与Session有什么区别?
| 对比 | JWT | Session |
|---|---|---|
| 服务端状态 | 通常较少 | 通常需要 |
| Token | JWT | Session ID |
| 分布式部署 | 比较方便 | 通常需要共享Session |
| 主动撤销 | 需要额外设计 | 相对简单 |
| API场景 | 常见 | 也可以使用 |
| 微服务 | 比较方便 | 需要额外架构 |
| Token大小 | 相对较大 | 通常较小 |
JWT并不一定比Session更安全。
真正重要的是:
认证方案是否正确设计和实现。
四十六、JWT与OAuth 2.0有什么区别?
JWT和OAuth 2.0经常一起出现,但它们不是同一个东西。
JWT:
一种Token格式。
OAuth 2.0:
一套授权框架。
可以理解为:
JWT
=
Token长什么样
OAuth 2.0
=
Token如何用于授权流程
OAuth 2.0的Access Token不一定必须是JWT。
JWT也不等于OAuth 2.0。
四十七、JWT在微服务中的典型架构
一个典型微服务系统:
┌──────────────┐
│ 认证服务 │
└──────┬───────┘
│
JWT Token
│
↓
┌───────────┐
│ API Gateway│
└─────┬─────┘
│
┌─────────────┼─────────────┐
↓ ↓ ↓
用户服务 订单服务 商品服务
用户登录:
客户端
↓
认证服务
↓
Access Token
+
Refresh Token
访问API:
客户端
↓
Gateway
↓
验证JWT
↓
业务服务
对于复杂系统,可以进一步设计:
- 认证中心
- API Gateway
- 权限中心
- Token黑名单
- Refresh Token管理
- 公钥服务
- 密钥轮换机制
四十八、JWT在API接口中的使用
例如登录接口:
POST /api/login
Content-Type: application/json
请求:
{
"username": "tom",
"password": "password"
}
服务器返回:
{
"accessToken": "xxxxx",
"refreshToken": "yyyyy",
"expiresIn": 900
}
之后调用用户接口:
GET /api/user/profile
Authorization: Bearer xxxxx
服务器验证:
Signature
+
exp
+
iss
+
aud
+
权限
验证成功:
{
"id": 10001,
"username": "tom"
}
四十九、JWT安全开发流程
开发JWT认证功能时,可以按照以下流程设计。
第一步:设计登录接口
POST /login
完成:
- 用户名验证
- 密码验证
- 账号状态检查
第二步:生成Access Token
包含必要Claims:
sub
iat
exp
iss