普贤教程 › 生态包
JWT 实战
registry/jwt 是纯函数实现的 JWS(HS256):签发、验签、过期/未生效校验,全部是本地计算,不发网络请求、不依赖 OpenSSL。
安装与最小示例
bash
pxpkg add jwt@0.2.0 && pxpkg install
px
import jwt def main(): var secret = "0123456789abcdef0123456789abcdef" var payload = {"sub": "u1001", "name": "张三", "iat": 1762340000, "exp": 1762343600} var tk = jwt_encode(payload, secret).ok() print(tk) var r = jwt_decode_at(tk, secret, 1762341000) # now 用显式秒,便于测试 print(r.ok()["payload"]) # {exp: …, iat: …, name: 张三, sub: u1001}
实际输出(本机跑出来的):
text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjIzNDM2MDAsImlhdCI6MTc2MjM0MDAwMCwibmFtZSI6IuW8oOS4iSIsInN1YiI6InUxMDAxIn0.Mbn07oTJFR6qu2AafE5qPcHviSWu7ptuHq8eezJTC2c
{exp: 1762343600, iat: 1762340000, name: 张三, sub: u1001}三个函数覆盖 90% 场景
| 函数 | 用途 | 返回 |
|---|---|---|
jwt_encode(payload, secret, alg="HS256") | 签发 | Ok(token) / Err |
jwt_decode(token, secret) | 校验 + 解出(用当前时间) | Ok({header, payload}) / Err |
jwt_decode_at(token, secret, now) | 同上,时间由你给(单测必用) | 同上 |
0.2.0 另带 jwt_encode_v2 / jwt_decode_v2 / jwt_decode_at_v2 与 RS256 的 jwt_rs256_sign / jwt_rs256_verify。
验签会拦住什么(实测)
| 情况 | 结果 |
|---|---|
| 密钥正确、未过期 | Ok |
| 密钥错误 | Err(jwt: 签名验证失败) |
| 已过期(now > exp) | Err(jwt: token 已过期) |
| 未到生效时间(now < nbf) | Err(jwt: token 尚未生效) |
| 篡改末 4 字符 | Ok 变 Err(签名不符) |
不是三段(abc.def) | Err(jwt: token 必须为 3 段(h.p.s)) |
| payload 里没有 exp | 照样通过——过不过期由你写不写 exp 决定 |
没有 exp 的 token 永不过期签发时忘了写
exp,jwt_decode 不会替你补判。生产签发一律显式写 exp(秒);也别信 iat 能当过期用。JWT 不是加密,别往里塞敏感数据
payload 只是 base64url,谁都能读:
px
print(jwt_b64url_decode(split(tk, ".")[1]).ok()) # {"exp":1762343600,"iat":1762340000,"name":"张三","sub":"u1001"} ← 明文
px
print(jwt_b64url_decode(split(tk, ".")[0]).ok()) # {"alg":"HS256","typ":"JWT"}
所以:手机号、邮箱、余额这类可以放(本来也会被业务读出来);密码、身份证号、密钥不要放。要保密就只放一个 id,敏感数据每次回查。
在 Web 接口里用(Ma px_routes)
px
import jwt def unauth(msg): print("Status: 401 Unauthorized") print("Content-Type: application/json; charset=utf-8") print("") print("{\"ok\":false,\"error\":\"" + msg + "\"}") def main(): var secret = env("JWT_SECRET") # ← 密钥从环境变量来,不写死 var cookie = env("HTTP_COOKIE") var token = "" var parts = split(cookie, "; ") for p in parts: if starts_with(p, "token="): token = p[6:len(p)] if token == "": unauth("缺少令牌") return var r = jwt_decode(token, secret) if r.is_err(): unauth(str(r.err())) return var uid = r.ok()["payload"]["sub"] print("Status: 200 OK") print("Content-Type: application/json; charset=utf-8") print("") print("{\"ok\":true,\"uid\":\"" + str(uid) + "\"}")
两个硬规矩① 密钥走环境变量(
env("NAME")),不写进源码和配置文件;② 日志里绝不打印完整 token——它是可重放的凭证,jwt_decode 解开的内容也别整段写日志。坑
1. HS256 的强依赖是密钥长度
HS256 的安全性等于密钥熵。用 "secret"、"123456" 这类密钥,等于没签。至少 32 字节随机,用 sr_hex(32)(registry/secure_random)生成。
px
import secure_random var key = sr_hex(32).ok() # 64 个 hex 字符
2. 时间单位是秒,不是毫秒
exp / nbf / jwt_decode_at 的 now 全是 Unix 秒。把 now_ms() 传进去,token 会「永不过期」——因为 1762340000000 远大于 exp。
px
var now_sec = int(now_ms() / 1000) # 正确
3. 过期不等于吊销
JWT 无状态,签出去就收不回。需要「立刻踢人」就得维护黑名单(Redis SET + TTL 正好),或在 token 里放 ver 字段、用户改密码时 +1 比对。
4. 别自己拼 base64url
标准 base64 的 + / = 在 URL 里会坏。库里的 jwt_b64url_encode / jwt_b64url_decode 已经处理好 padding,直接用。
和 Session 怎么选
| JWT | 服务端 Session(Redis / 库) | |
|---|---|---|
| 可撤销 | 难(要黑名单) | 删 key 即可 |
| 无状态 | 是,多机免同步 | 要共享存储 |
| 体积 | 每请求带几百字节 | 几十字节 cookie |
| 适用 | API、跨服务、短期令牌 | Web 站点会话 |
本站这类需登录的站点,一般「短期 JWT(15 分钟)+ Redis 刷新令牌」是折中方案:access 无状态,refresh 可撤销。