px普贤教程PuXian
普贤教程 › 生态包

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 可撤销。