
看完这篇你会知道:Token 怎么签发、怎么验证、过期怎么处理、被篡改了会怎样。
准备工作
1. 安装库
只需要一个库:
pip install PyJWT
注意包名是 PyJWT,不是 jwt(有个同名的老包,会冲突)。导入的时候用 import jwt。
2. 生成密钥
还需要一个密钥。JWT 的安全性全靠这个密钥,谁拿到它,谁就能伪造任意 Token。所以密钥必须足够长、足够随机、不能写在代码里。
生成方式很简单,用 Python 标准库或命令行都行:
import secrets
secret_key = secrets.token_hex(32) # 32字节-> 64 个十六进制字符,256 bit
为什么参数写 32,出来却是 64 个字符?因为 token_hex 先生成指定数量的随机字节,再把每个字节转成两位十六进制:
secrets.token_hex(32)的参数 32 是字节数(bytes)token_hex 的流程:先生成 32个随机字节,然后把每个字节转成两位十六进制字符。所以 32 字节= 64个十六进制字符,即256 bit。
一个字节的取值范围是 0~255,刚好用两位十六进制(00~ff)表示。所以 32 字节展开后就是 64 个字符,信息量是 32 × 8 = 256 bit,这个长度对 HS256 签名来说绰绰有余。

-
或者使用openssl
openssl rand -hex 32
生成之后放到环境变量里,代码通过 os.environ 读取。
一、第一步:签发一个 Token
先做最简单的事,把用户信息打包成一个 JWT:
import jwt
import secrets
# 服务器的密钥,只有服务器知道
SECRET_KEY = secrets.token_hex(32)
# 用户信息(Payload)
payload = {
"user_id": 123,
"name": "zhangsan",
"role": "user"
}
# 签发 Token
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
print(token)
运行结果是一串用点号分隔的字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiJ6aGFuZ3NhbiIsInJvbGUiOiJ1c2VyIn0.S7SMJTQKubOha1ItdBr3G5Nxb3BKwGPTFWxtTKPs3Ac
JWT各部分组成解释如下:

1.1 jwt.encode()
jwt.encode 做了三件事:构建 Header、对 Payload 做 Base64 编码、用密钥生成 Signature。

三步就像一条流水线:
- 第一格 PyJWT 自动填好算法和类型生成 Header,不需要你手动构造;
- 第二格把你传入的 payload 字典做 Base64url 编码,变成字符串,注意是编码不是加密,谁都能解回来;
- 第三格才是安全的关键,用密钥对前两段的内容做 HMAC-SHA256 运算生成签名。最终三段用点号拼接,就是你在终端里看到的那串 Token。
1.2 手工验证
自己手动编码一遍就知道了。
payload = {"user_id": 123, "name": "zhangsan", "role": "user"}
(1)将字典变形
json.dumps() 把字典变成 JSON 字符串,.encode() 再把字符串变成 bytes。因为base64 只认 bytes,所以这两步缺一不可。
import json
json.dumps(payload).encode()
转换后的内容为:
b'{"user_id": 123, "name": "zhangsan", "role": "user"}'
(2) base64编码
import base64
encoded = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b"=")
print(encoded.decode())
# eyJ1c2VyX2lkIjogMTIzLCAibmFtZSI6ICJ6aGFuZ3NhbiIsICJyb2xlIjogInVzZXIifQ
-
rstrip(b"=")是因为 JWT 规范要求去掉 Base64 末尾的填充符,所以 Token 里看不到=,解码时补回来就行。如果不使用这个方法对右侧的=进行去除,编码后的结果如下:b'eyJ1c2VyX2lkIjogMTIzLCAibmFtZSI6ICJ6aGFuZ3NhbiIsICJyb2xlIjogInVzZXIifQ==' -
使用
urlsafe_b64encode而不是普通的b64encode。区别是把+换成-、/换成_,这样 Token 放在 URL 和 HTTP Header 里不会被转义。 -
最后,
encoded是 bytes 类型(b"eyJ1c2Vy..."),.decode()把它转成普通字符串"eyJ1c2Vy...",这样 print 输出时不会带b'...'前缀。这里只是为了打印好看,和JWT过程无关。 -
如果想要和JWT最终Token里用点好分隔的中间段完全一致,还需要进行额外的处理。
encoded = base64.urlsafe_b64encode( json.dumps(payload, separators=(",", ":")).encode() ).rstrip(b"=")separators=(",", ":")不能少。json.dumps默认会在冒号和逗号后加空格("user_id": 123),而 PyJWT 用的是紧凑格式("user_id":123,无空格)。JSON 字符串不同,Base64 结果自然不同。少了这个参数,编出来的就和 Token 里的对不上。eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiJ6aGFuZ3NhbiIsInJvbGUiOiJ1c2VyIn0
(3) base 64解码
反过来也能解回来,urlsafe_b64decode() 是 Base64 解码,把编码后的字符串还原成原始 JSON。
decoded = base64.urlsafe_b64decode(encoded + b"==")
print(json.loads(decoded))
# {'user_id': 123, 'name': 'zhangsan', 'role': 'user'}
所以 Payload 这段真的就是"裸着的",任何人拿到 Token 都能解出里面写了什么。安全靠的不是藏,而是第三步的签名。只能看,但是不能改。
二、第二步:验证并解析 Token
服务器收到客户端带来的 Token,需要验签并取出用户信息:
# 验签 + 解析
data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print(data)
{'user_id': 123, 'name': 'zhangsan', 'role': 'user'}
jwt.decode 做的事:用密钥重新算一次签名,和 Token 里的 Signature 比对。对得上就返回 Payload 内容,对不上就抛异常。
注意 algorithms 参数是列表,而且必须显式传。这是一个安全设计,防止攻击者在 Header 里把算法改成 none 绕过签名。
三、第三步:看看篡改会怎样
改了 Payload 但没有密钥,签名就对不上,我们可以验证一下。
思路很简单:Token 是三段用点号拼接的字符串,Payload 那段只是 Base64 编码,谁都能解开。
那可以试试解开、改掉、再拼回去,能不能骗过服务器?
3.1 正常签名
先签一个正常的 Token:
import jwt
SECRET_KEY = "my-secret-key"
payload = {
"user_id": "Alen",
"role": "user",
"age": 18
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
此时的token为:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiQWxlbiIsInJvbGUiOiJ1c2VyIiwiYWdlIjoxOH0.PeZwOEdZKB3PLXgOepzQdQBm7s0LhWWIrfJdmxGtdZo
3.2 篡改信息
然后,我们模拟攻击者的操作。把 Token 拆开,解码中间那段 Payload,把 role 从 user 改成 admin:
def fake_payload(token):
parts = token.split(".")
payload_bytes = base64.urlsafe_b64decode(parts[1] + "==")
payload = json.loads(payload_bytes)
print(f"原始 Payload: {payload}")
payload["role"] = "admin"
faked_payload = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b"=").decode()
return faked_payload
faked_payload = fake_payload(token)
关键一步:把篡改后的 Payload 塞回去,但 Signature 还是原来的。因为攻击者不知道密钥,没法重新签名:
parts = token.split(".")
fake_token = f"{parts[0]}.{faked_payload}.{parts[2]}"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjogIkFsZW4iLCAicm9sZSI6ICJhZG1pbiIsICJhZ2UiOiAxOH0.PeZwOEdZKB3PLXgOepzQdQBm7s0LhWWIrfJdmxGtdZo
篡改后的token虽然前两段和原先的token一样,但是signature部分不同。
3.3 验签
拿这个伪造的 Token 去验签:
try:
data = jwt.decode(fake_token, SECRET_KEY, algorithms=["HS256"])
print(f"验签通过: {data}")
except jwt.InvalidSignatureError:
print("验签失败!Token 被篡改了")
验签失败!Token 被篡改了
服务器拿密钥对新的 Payload 重算了一遍签名,发现跟 Token 里带的 Signature 对不上,直接拒绝。Payload 是"明文"没错,但签名把它锁死了,只能看,不能改。
四、第四步:加上过期时间
真实场景中 Token 不能永远有效。JWT 用 exp(expiration)字段控制过期。
4.1 设置过期时间
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "my-secret-key"
payload = {
"user_id": 123,
"name": "zhangsan",
"exp": datetime.now(timezone.utc) + timedelta(hours=2) # 2小时后过期
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
-
exp是 JWT 的标准字段,PyJWT 会自动处理:验签时如果发现当前时间超过了exp,直接抛ExpiredSignatureError。 -
datetime.now(timezone.utc)拿到的是当前的 UTC 时间,一个精确到微秒的时间点对象。参数timezone.utc不能省,省了拿到的是本地时间,服务器部署在不同时区就会出 bug。 -
timedelta是"时间差",表示一段持续时间而不是某个时间点。timedelta(hours=2)就是"2 小时"这个时间长度,timedelta(seconds=1)就是"1 秒"。两者相加减就是普通的时间算术:
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
print(f"now = {now}")
print(f"2 hours later = {now + timedelta(hours=2)}")
print(f"1 second before = {now - timedelta(seconds=1)}")
now = 2026-03-26 01:29:59.696712+00:00 2 hours later = 2026-03-26 03:29:59.696712+00:00 1 second before = 2026-03-26 01:29:58.696712+00:00
所以 exp 那行做的事很直白:拿到现在的时间,往后加 2 小时,就是过期时间。PyJWT 收到这个 datetime 对象后,会自动把它转成 Unix 时间戳(一个整数)写进 Token 里。验签的时候再用 datetime.now(timezone.utc) 取当前时间和 exp 比较,超过了就抛异常,没超过就放行。
4.2 模拟过期情况
# 模拟过期的情况
expired_payload = {
"user_id": 123,
"exp": datetime.now(timezone.utc) - timedelta(seconds=1) # 1秒前已过期
}
expired_token = jwt.encode(expired_payload, SECRET_KEY, algorithm="HS256")
try:
jwt.decode(expired_token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
print("Token 已过期,请重新登录")
Token 已过期,请重新登录
注意,这里不需要自己写时间判断逻辑,PyJWT 内置处理了。
五、和理论篇的对照
| 理论概念 | 对应代码 |
|---|---|
| Header + Payload + Signature | jwt.encode() 自动构建三段 |
| 服务器用密钥签名 | SECRET_KEY 参数 |
| 验签:重算签名并比对 | jwt.decode() 自动完成 |
| Payload 可以被看到但不能被改 | 篡改实验:改了 Payload,验签失败 |
| Token 过期机制 | exp 字段 + ExpiredSignatureError |
理论和代码一一对应,没有黑盒。JWT 的"魔法"拆开看就是:Base64 编码 + HMAC 签名 + JSON,全是基础工具的组合。
3096



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



