Json Web Token
约 5194 字大约 17 分钟
2025-10-16
什么是JWT?
JWT(Json Web Token) 是目前最流行的跨域认证解决方案,是一种基于Token的认证授权机制。从JWT的全程可以看出,JWT本身也是Token,是一种规范化后的JSON结构的Token.
JWT自身包含了身份验证所需的所有信息,因此,我们的服务器不需要存储Session信息。这显然增加了系统的可用性与伸缩性,大大减轻了服务端的压力。
可以看出,JWT更符合 RESTful API 的 Stateless(无状态) 原则。
并且,使用JWT认证可以有效避免CSRF攻击,因为JWT一般存在localStorage中, 使用JWT进行身份验证的过程中是不会涉及到Cookie的。
下面是RFC7519对JWT做的较为正式的定义。 
JWT由哪些部分组成?

JWT本质上就是一组字符串,通过 . 切分成三个Base64编码的部分:
Header(头部):描述 JWT 的元数据,定义了生成签名的算法以及
Token的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。Payload(载荷):用来存放实际需要传递的数据,包含声明(Claims),如
sub(subject,主题)、jti(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。
JWT通常是这样的:xxxxxxxxxxx.yyyyyyyyyy.zzzzzzzz
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30你可以在jwt.io这个网站上对JWT进行解码,解码之后得到的就是Header、Payload、Signature这三个部分。
Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

头部 Header
Header通常由两部分组成:
typ(Type): 令牌类型,也就是 JWTalg(Algorithm): 签名算法,比如 HS256。
示例:
{
"alg": "HS256",
"typ": "JWT"
}JSON 格式的Header被 Base64Url 编码,成为JWT的第一部分。
载荷 Payload
Payload也是JSON格式数据,其中包含了Claims(声明,包含了JWT的相关信息)。
Claims分为三种类型:
- Registered Claims(注册声明):预定义的一些声明,建议使用,但非强制性。
- Public Claims(公有声明):JWT签发方可以自定义的声明,但是为了避免冲突,应该在IANA JSON Web Token Registry中定义它们。
- Private Claims(私有声明):JWT签发方因为项目需要而自定义的声明,更符合实际项目场景使用。
下面是一些常见的注册声明:
iss(issue):JWT签发方iat(issued at time):JWT签发时间sub(subject):JWT主题aud(audience):JWT接收方exp(expiration time):JWT的过期时间nbf(not before time):JWT生效时间,早于该定义时间的JWT不能被接受处理jti(JWT ID):jwt唯一标识
示例:
{
"uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"sub": "1234567890",
"name": "John Doe",
"exp": 15323232,
"iat": 1516239022,
"scope": ["admin", "user"]
}JSON格式的Payload被 Base64Url 编码,成为JWT的第二部分。
签名 Signature
Signature 是通过对 JWT 的前两部分(Header 和 Payload)进行加密签名得到的,作用是防止 JWT(主要是 payload) 被篡改。
生成Signature时需要用到:
- 编码后的 Header + Payload
- 存放在服务端的密钥 Secret (重要!一定不能泄露!)
- Header中的
alg(签名算法)
签名的计算公式如下:
signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)算出签名以后,把Header、Payload、Signature三部分用.拼接成一个字符串,这个字符串就是JWT。
重要提示:任何人都可以解码 JWT 的 Header 和 Payload,因为它们只是经过 Base64Url 编码,并非加密。所以绝对不要在 Payload 中存放敏感信息(如密码)。Signature 的作用是保证信息不被篡改,而不是保证信息不被看见。
如何基于JWT进行身份验证?
在基于JWT身份认证的应用程序中,服务端通过Payload、Header、Secret创建JWT并将JWT发送给客户端。
客户端收到JWT后,将其保存至Cookie或localStorage中,以后客户端发出的所有请求都会携带这个令牌。

第一步:获取令牌(Login)
- 用户登录:客户端将用户名和密码通过请求登录认证接口发送至服务端,例如
POST /api/auth/login - 服务器验证凭证:服务端收到请求后检查数据库,验证用户名和密码是否匹配,用户是否有效
- 生成JWT:验证成功后,服务端生成JWT
- 返回JWT:服务器将JWT返回至客户端,通常将JWT放在JSON响应体中返回:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer" } - 客户端存储令牌:客户端收到JWT后,将JWT存储至本地,常见的存储方式有
localStorage、Cookie建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
第二步:使用令牌访问受保护资源(Access Protected Resources)
客户端发送请求:此后,客户端每次请求受保护资源或API时(例如
GET /api/profile),都必须在请求中携带JWT- 标准做法是在HTTP Header的
Authorization字段中携带:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- 标准做法是在HTTP Header的
服务端验证令牌:服务端收到请求之后,会执行以下步骤:
提取JWT:从Http Header的
Authorization字段中提取JWT令牌验证签名:
- 服务端解析出客户端JWT的Header、Payload,然后结合自己的Secret按照相同加密算法重新生成一个Signature,新Signature和客户端JWT中的Signature进行比对。完全吻合即验证通过,否则验证失败。
返回响应:
如果验证通过:服务器认为请求来自一个合法用户。它可以从 JWT 的 Payload 中直接解析出用户身份(如用户ID),而无需再去查询数据库。然后处理请求并返回相应的数据(如
200 OK和用户资料)。如果验证失败(签名无效、过期、格式错误等):服务器返回
401 Unauthorized错误,要求客户端重新登录获取新的令牌。
如何防止JWT被篡改?
有了签名之后,即使JWT被泄露或截获,黑客也无法同时篡改Signature、Header、Payload。
这是为什么呢?因为密钥Secret存储在服务端,服务端拿到客户端JWT后,会解析出客户端JWT的前两部分,然后加上自己的Secret重新生成一份Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。
不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。
密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
JWT的优缺点分析
无状态
JWT自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储JWT信息。这显然增加了系统化的可用性和伸缩性,大大减轻了服务端的压力。
不过,也正是由于JWT无状态,也导致了它最大的缺点:不可控!
比如说,我们想要在JWT有效期内废弃这个JWT或者更改它的权限时,并不会立即生效,通常需要等到有效期过后才可以。
再比如说,当用户Logout的话,JWT依然是有效状态。除非我们在服务端增加额外的处理逻辑,如将失效的JWT存储起来,后端先验证JWT是否有效在进行处理。
有效避免了CSRF攻击
CSRF(Cross Site Request Forgery)一般被称为跨站请求伪造,属于网络攻击范畴。相比于SQL脚本注入、XSS等安全攻击方式,CSRF的知名度并没有它们高。但是,它的确是我们开发系统时必须考虑的安全隐患。强如Google Gmail也曾在2007年爆出过CSRF漏洞,当时给Gmail用户造成了很大损失。
那么究竟什么是跨站请求伪造呢?简单来说就是一种“借刀杀人”的网络攻击手法。攻击者欺骗你的浏览器,让你在已登录某个网站(如银行)的状态下,不知不觉地发出一个恶意请求(如转账),而你自己却完全不知道。
举个简单例子:小庄登陆了某网上银行,他来到了网上银行的讨论区,看到一个帖子下面有一个写着“科学理财,年盈利率过万”的链接,小庄好奇地点开了这个链接,结果发现自己的账户少了10000元。发生了什么呢?原来黑客在链接中藏了一个请求,这个请求直接利用小庄的身份向银行发送了一个转账请求,也就是通过你的Cookie像银行发出请求。
<a src="http://www.mybank.com/Transfer?bankId=小庄的银行卡id&money=10000"
>科学理财,年盈利率过万</a
>CSRF攻击需要依赖Cookie,在小庄登录银行网站后,服务端返回给客户端的SessionId会存储在本地Cookie中,客户端Cookie自动发送机制使得只要小庄点击了黑客链接,小庄的SessionId就会被Cookie发送至服务端,服务端鉴权通过后黑客就拥有了小庄银行账号的一切权限,请求一次转账API不在话下。
另外,并不是必须点击链接才可以达到攻击效果,很多时候,只要你打开了某个页面,CSRF攻击就会发生。
为什么JWT会避免CSRF攻击呢?

一般情况下,我们登录成功获得JWT后,会选择存放在localStorage中。当客户端请求受限资源时会去localStorage找JWT,携带JWT请求服务端,过程中压根不会涉及到Cookie。因此,即使你点击了黑客的非法链接,黑客也无权读取你客户端的localStorage(因为受到同源策略保护),黑客连JWT都拿不到,更别说服务端鉴权了,直接失败。
总结来说就一句话:使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。
不过,这样也会存在 XSS 攻击的风险。为了避免 XSS 攻击,你可以选择将 JWT 存储在标记为httpOnly 的 Cookie 中。但是,这样又导致了你必须自己提供 CSRF 保护,因此,实际项目中我们通常也不会这么做。
常见的避免 XSS 攻击的方式是过滤掉请求中存在 XSS 攻击风险的可疑字符串。
在Django项目中,我们是通过 Python 的 bleach 库实现创建xss过滤器的:
# utils/security.py
import bleach
from django.utils.html import strip_tags
# 定义一个允许的 HTML 标签和属性的白名单
# 这是一个非常严格的设置,默认只允许纯文本。你可以根据需要扩展。
ALLOWED_TAGS = bleach.sanitizer.ALLOWED_TAGS + ['p', 'br', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
ALLOWED_ATTRIBUTES = {
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
'a': ['href', 'title', 'target', 'rel'],
'img': ['src', 'alt', 'width', 'height'],
'*': ['class', 'style'] # 允许所有标签有 class 和 style 属性(谨慎使用)
}
def clean_xss(text):
"""
使用 bleach 库来清理文本,防止 XSS。
保留白名单中允许的 HTML 标签和属性,其他都会被转义或删除。
参数:
text (str): 待清理的文本
返回:
str: 清理后的安全文本
"""
if text is None:
return None
# 首先使用 bleach.clean 进行清理
cleaned_text = bleach.clean(text, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
# 如果还想彻底移除所有HTML标签,只留纯文本,可以使用 strip_tags
# cleaned_text = strip_tags(cleaned_text)
return cleaned_text
def clean_xss_full(text):
"""
彻底清理文本,移除所有 HTML 标签,只返回纯文本。
这是最安全的选项。
参数:
text (str): 待清理的文本
返回:
str: 纯文本
"""
if text is None:
return None
return strip_tags(text)在视图中使用过滤器:
# views.py
from django.http import JsonResponse
from .models import Article
from utils.security import clean_xss, clean_xss_full # 导入我们写的过滤器
def create_article(request):
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
# 在保存到数据库之前进行 XSS 清理
safe_title = clean_xss_full(title) # 对标题,彻底移除所有HTML
safe_content = clean_xss(content) # 对内容,允许一些基本的HTML格式
# 创建并保存文章
article = Article(
title=safe_title,
content=safe_content
)
article.save()
return JsonResponse({'status': 'success', 'message': 'Article created safely!'})
return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)适合移动端应用
使用Session进行身份认证的话,需要保存一份信息在服务端,而且这种方式会依赖Cookie(需要保存SessionId),所以不适合移动端。
为什么使用 Session 进行身份认证的话不适合移动端 ?
- 状态管理: Session 基于服务端的状态管理,而移动端应用通常是无状态的。移动设备的连接可能不稳定或中断,因此难以维护长期的会话状态。如果使用 Session 进行身份认证,移动应用需要频繁地与服务器进行会话维护,增加了网络开销和复杂性;
- 兼容性: 移动端应用通常会面向多个平台,如 iOS、Android 和 Web。每个平台对于 Session 的管理和存储方式可能不同,可能导致跨平台兼容性的问题;
- 安全性: 移动设备通常处于不受信任的网络环境,存在数据泄露和攻击的风险。将敏感的会话信息存储在移动设备上增加了被攻击的潜在风险。
但是,使用JWT进行身份认证就不会存在此问题,因为JWT能被存储至本地,即使被暴露也没有数据泄露的风险。且JWT还可以跨语言使用。
单点登录友好
传统的 Session 单点登录:服务端应用收到一个 SessoinId 后,必须拿着这个 SessoinId 执行鉴权操作,问:“这个用户是谁?这个SessoinId有效吗?” 这是一个网络IO操作,有延迟,且认证中心成了单点故障和性能瓶颈。
JWT 单点登录:服务端自己用预先配置的密钥验证 JWT 的签名。只要签名有效、未过期、接收方是自己,它就完全信任Token里写明的用户信息。无需验证身份。
JWT体积太大
JWT 结构复杂(Header、Payload 和 Signature),包含了更多额外的信息,还需要进行 Base64Url 编码,这会使得 JWT 体积较大,增加了网络传输的开销。
解决办法:
- 尽量减少 JWT Payload(载荷)中的信息,只保留必要的用户和权限信息。
- 在传输 JWT 之前,使用压缩算法(如 GZIP)对 JWT 进行压缩以减少体积。
- 在某些情况下,使用传统的 Token 可能更合适。传统的 Token 通常只是一个唯一标识符,对应的信息(例如用户 ID、Token 过期时间、权限信息)存储在服务端,通常会通过 Redis 保存。
JWT身份认证常见问题及解决办法
注销登录后JWT仍然有效的问题
与之类似的具体场景还有:
- 退出登录
- 修改密码
- 服务端修改了某个用户的权限或角色
- 用户账户被封禁/删除
- 用户被服务端强制注销
- 用户被踢下线
- ...
这个问题不存在于Session认证方式中,因为在Session认证方式中,遇到这种情况时服务端删除对应用户的Session记录即可。但是,使用JWT认证方式时就不好解决了。我们之前说过,JWT一旦派发出去,如果服务端不增加其他逻辑的话,它在失效之前都是有效的。
那我们如何解决这个问题呢?以下是4中方案:
将JWT存入数据库
将有效的JWT存入数据库中,建议使用内存数据库比如Redis。如果需要让某个JWT失效就直接从Redis中删除这个JWT接口。但是,这样做会导致每次使用JWT都要先从Redis中查询JWT是否存在,违背了JWT无状态的原则。
黑名单机制
和上面的方法类似,使用内存数据库比如Redis维护一个黑名单,如果想让某个JWT失效的话就直接将这个JWT加入到黑名单即可。此后服务端每次对JWT判断是否在黑名单中。一样的,也违背了JWT无状态的原则。
前两种方案的核心在于将有效/无效的JWT存储起来,在进行JWT鉴权之前增加了一个判断是否在数据库中的逻辑
修改密钥(Secret)
为每个用户都创建一个专属密钥,如果想让某个JWT失效,直接修改对应用户的密钥即可。但是,这种方式比前两种存入数据库的方式危害更大:
- 如果服务是分布式的,则每次发出新的JWT时必须多台服务器同步密钥。为此,需要将密钥存储在数据库或其他外部服务中,这样和Session认证就没太大区别了
- 如果用户同时在多个终端打开系统,如果它从一个地方将账号对出,那么在其他终端都要重新登录,这是不可取的。
缩短令牌的有效期限并经常轮换
很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。
JWT的续签问题
JWT有效期一般都建议设置的不太长,那么JWT过期后如何认证?如何实现动态刷新JWT从而避免用户经常需要重新登录?
我们先来看看Session认证中一般的做法:假如Session的有效期是30min,如果30min内用户有访问,就把Session有效期延长30min
JWT认证的话如何解决续签问题呢?查阅了很多资料,我简单总结了下面 4 种方案:
类似于 Session 认证中的做法(不推荐)
这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。
每次请求都返回新 JWT(不推荐)
这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
JWT 有效期设置到半夜(不推荐)
这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
用户登录返回两个 JWT(推荐)
第一个是 accessJWT ,它作为访问受限资源的JWT,过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。refreshJWT 只用来获取 accessJWT,不容易被泄露。
客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,客户端就将 refreshJWT 传给服务端。如果refreshJWT有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。
这种方案也有不足之处:
需要客户端来配合
用户注销的时候需要同时保证两个 JWT 都无效
重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT)
存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。不过,由于 refreshJWT 只用来获取 accessJWT,不容易被泄露。
