bigywater.tech
Flask · 47

Signed Cookie

Flask 的 session 与「记住我」cookie 靠什么保证不被篡改?值里到底装了什么?能不能自己算出签名?过期和撤销怎么处理?这篇把 itsdangerous 的算法手工复现一遍。

3 段
签名后 token 的段数:payload、timestamp、signature
20 字节
HMAC-SHA1 签名的长度
明文
payload 的状态(没有加密,只是签名)

签名后的结构

实测 · 本机执行输出
  token = eyJ1aWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ.ar8lEA.YELxoY24nfIUV09xiWo6eGrKZlU
  分成 3 段: [34, 6, 27] 字符
  第 1 段(payload)base64 解码 → {"uid":42,"role":"admin"}
  第 2 段(时间戳)解码为整数 → 1790911760 → 2026-10-02T03:29:20+00:00
  第 3 段(签名)解码后的字节数 → 20 (HMAC-SHA1 的输出是 20 字节)
  结论:payload 没有加密,任何人拿到 cookie 都能读;签名只保证「没被改过」。

上一篇:Cookie 的读写过程;下一篇:Session Cookie 的数据结构;SECRET_KEY 的用途:SECRET_KEY 的用途。

手工复现签名

实测 · 本机执行输出
  手算签名 = YELxoY24nfIUV09xiWo6eGrKZlU
  token 里的签名 = YELxoY24nfIUV09xiWo6eGrKZlU
  相同: True
  派生密钥 = sha1(salt + "signer" + secret_key) 的前 8 字节(十六进制): 095499a3706d5680

篡改与伪造

实测 · 本机执行输出
  原样                           → 通过 → {'uid': 42, 'role': 'admin'}
  改 payload(签名不变)              → BadTimeSignature: Signature b'YELxoY24nfIUV09xiWo6eGrKZlU' does not match
  改时间戳(签名不变)                   → BadTimeSignature: Signature b'YELxoY24nfIUV09xiWo6eGrKZlU' does not match
  用另一个密钥验证                     → BadTimeSignature: Signature b'YELxoY24nfIUV09xiWo6eGrKZlU' does not match
  用另一个 salt 验证                 → BadTimeSignature: Signature b'YELxoY24nfIUV09xiWo6eGrKZlU' does not match
  截断                           → BadTimeSignature: Signature b'YELxoY24nfIUV09xiWo6eG' does not match
  去掉签名段                        → BadSignature: Signature b'ar8lEA' does not match
  随机字符串                        → BadSignature: No b'.' found in value

过期

实测 · 本机执行输出
  max_age=10(未过期)              → 通过 → x
  max_age=1(已过期)               → SignatureExpired: Signature age 2 > 1 seconds
  SignatureExpired 的 date_signed = 2026-10-02T03:29:20+00:00 | payload(base64 形式)仍然可取 = b'Ingi'
  不传 max_age 时永不过期: x

Flask 里的使用

实测 · 本机执行输出
  Set-Cookie 的第一段(名字=值): remember=eyJ1aWQiOjd9.ar8lEg.bluLY8uwxmCwKa1e2BKRZHA9gCw…
  GET /me → 200 {'uid': 7}
  篡改 payload 后 GET /me → 401 {'error': 'bad signature'}

没有撤销

实测 · 本机执行输出
  登出(客户端删 cookie)之后,旧 token 被别人拿着继续发 → 200 (服务器无从得知它已「登出」)

大小与压缩

实测 · 本机执行输出
  payload                       JSON 字节    token 字符  压缩标记
  10 字符的随机十六进制                       12          51  未压缩
  100 字符的随机十六进制                     102         142  以 . 开头(已压缩)
  1000 字符的随机十六进制                   1002         786  以 . 开头(已压缩)
  100 个重复的 x                        102          54  以 . 开头(已压缩)
  1000 个重复的 x                      1002          60  以 . 开头(已压缩)

分步演示

签名 cookie
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。itsdangerous 2.2.0
    • 没有验证:Fernet 等加密方案(本篇的签名不加密)、签名算法换成 SHA-256 的方式、SECRET_KEY_FALLBACKS 轮换(见第 187 篇)、时钟漂移对 max_age 的影响
    京ICP备2026067502号-1京公网安备11010502063487号