TT Lab
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

签发 __Host- 会话 Cookie 并正确删除

在 TT Lab 中继续学习

目标

在 127.0.0.1:8301 上搭建登录服务器,签发、检查并撤销 __Host-sid 会话 Cookie,并确认该 Cookie 确实被浏览器(curl)的存储接受并删除。

为什么重要

Cookie 问题从服务器日志里看不出来。服务器正常发出了 Set-Cookie,浏览器却因前缀规则或 Secure 条件悄悄丢弃,症状就只表现为“偶尔登录不了”。所以本实验在看响应头的同时,还要看 curl 的 Cookie 存储。curl 8.5 也不会保存违反 __Host-、__Secure- 规则的 Cookie,因此存储中是否留下,就近似于是否被接受。退出登录也用同样的眼光来看。删除 Cookie 只是对浏览器的请求,对于复制了该值的人来说,只有服务器记录是否被删除才有意义。

步骤

  1. 在 127.0.0.1:8301 上启动 /root/st/cookie/app.py。GET /healthz 返回 200 和 {"ok":true}。
  2. 向 POST /login 发送 JSON {"user":"alice","password":"wonderland"},返回 200 和 {"user":"alice"},并签发 __Host-sid Cookie。值是用 secrets 生成的至少 128 位随机数(base64url 至少 22 个字符),每次登录都必须不同,且不能包含用户名。bob/builder 也能登录。
  3. 给会话 Cookie 加上 Secure、HttpOnly、SameSite=Lax、Path=/,不加 Domain 和 Max-Age/Expires(仅主机的会话 Cookie)。以 alice 登录,并用 curl -c /root/st/cookie/jar.txt 保留 Cookie 存储。该文件中的 __Host-sid 行必须以 #HttpOnly_127.0.0.1 开头,路径为 /,Secure 为 TRUE,过期时间为 0。
  4. GET /me 在存在有效的 __Host-sid 时返回 200 和 {"user":"<사용자>"}(占位符为用户名);没有 Cookie,或值不是服务器签发过的值时,返回 401。
  5. 错误密码(alice/wrongpass)和不存在的用户(mallory)调用 POST /login,都返回 401,且一行 Set-Cookie 都不能有。
  6. POST /logout 返回 200 并删除服务器端会话。响应中必须有删除 __Host-sid 的 Set-Cookie(Max-Age=0),而且 curl 存储必须真正接受该删除,使 Cookie 消失。用退出登录之前复制下来的 Cookie 值调用 GET /me,必须返回 401。第 3 步的 jar.txt 不要覆盖,改用其他存储文件来测试。
  7. 当 https://app.example.com/login 的响应带来下面的 Set-Cookie 值时,判定浏览器是否会保存该 Cookie,并在 /root/st/cookie/prefix.txt 中按 번호=accept 或 번호=reject(占位符为编号)每行写一条。
    • 1 __Host-sid=a1; Secure; HttpOnly; Path=/; SameSite=Lax
    • 2 __Host-sid=a2; HttpOnly; Path=/; SameSite=Lax
    • 3 __Host-sid=a3; Secure; Path=/; Domain=app.example.com
    • 4 __Host-sid=a4; Secure; Path=/account
    • 5 __Secure-theme=dark; Secure; Path=/account; Domain=example.com
    • 6 __Secure-theme=dark; Path=/
    • 7 __host-sid=a7; Path=/
    • 8 theme=dark; Path=/; Domain=example.com
  8. 用 /root/st/cookie/e2e.sh 依次测试登录、带 Cookie 访问 /me、不带 Cookie 访问 /me、错误密码、退出登录、重放旧 Cookie,并统计退出登录后存储中剩余的 __Host-sid 行数,将 login=200 me=200 no_cookie=401 bad_password=401 logout=200 replay=401 jar_after_logout=0 写成一行,保存到 /root/st/cookie/e2e.out。

参考

启动登录服务器

在 127.0.0.1:8301 上启动 /root/st/cookie/app.py。GET /healthz 返回 200 和 {"ok":true}。

用标准库 http.server 就足够了。把服务器放到后台运行,等 /healthz 有响应后再进入下一步。

登录时签发会话 Cookie

向 POST /login 发送 JSON {"user":"alice","password":"wonderland"},返回 200 和 {"user":"alice"},并签发 __Host-sid Cookie。值是用 secrets 生成的至少 128 位随机数(base64url 至少 22 个字符),每次登录都必须不同,且不能包含用户名。bob/builder 也能登录。

会话值是不携带任何含义的钥匙。random 模块的结果可以被预测,所以请在使用密码学随机数的 secrets 模块中,找到生成 URL 安全字符串的函数。这个会话属于谁,由服务器记录在 dict 中。

确认 Cookie 属性与存储

给会话 Cookie 加上 Secure、HttpOnly、SameSite=Lax、Path=/,不加 Domain 和 Max-Age/Expires(仅主机的会话 Cookie)。以 alice 登录,并用 curl -c /root/st/cookie/jar.txt 保留 Cookie 存储。该文件中的 __Host-sid 行必须以 #HttpOnly_127.0.0.1 开头,路径为 /,Secure 为 TRUE,过期时间为 0。

Domain 写得越多,范围越宽,省略才会只发给发送它的主机。不写过期时间,就是关闭浏览器时消失的会话 Cookie。curl 存储文件是用制表符分隔的七列,HttpOnly Cookie 会在第一列前面带上标记。

通过 Cookie 认出用户

GET /me 在存在有效的 __Host-sid 时返回 200 和 {"user":"<사용자>"}(占位符为用户名);没有 Cookie,或值不是服务器签发过的值时,返回 401。

仅仅是 Cookie 被附带过来,什么也证明不了。必须用这个值查找服务器的会话记录,从中取出用户。

登录失败时不发放 Cookie

错误密码(alice/wrongpass)和不存在的用户(mallory)调用 POST /login,都返回 401,且一行 Set-Cookie 都不能有。

如果生成 Cookie 的代码在密码检查之前,失败时也会发出 Cookie。密码比较最好使用不会泄露时间差的函数。

让退出登录真正生效

POST /logout 返回 200 并删除服务器端会话。响应中必须有删除 __Host-sid 的 Set-Cookie(Max-Age=0),而且 curl 存储必须真正接受该删除,使 Cookie 消失。用退出登录之前复制下来的 Cookie 值调用 GET /me,必须返回 401。第 3 步的 jar.txt 不要覆盖,改用其他存储文件来测试。

删除 Cookie 只是一个请求。对复制了该值的人来说,只有服务器记录是否被删除才有意义。而且删除用的 Cookie 也是 Cookie,必须重新通过 __Host- 前缀的条件,浏览器才会接受。

判定前缀规则

当 https://app.example.com/login 的响应带来下面的 Set-Cookie 值时,判定浏览器是否会保存该 Cookie,并在 /root/st/cookie/prefix.txt 中按 번호=accept 或 번호=reject(占位符为编号)每行写一条。1 __Host-sid=a1; Secure; HttpOnly; Path=/; SameSite=Lax · 2 __Host-sid=a2; HttpOnly; Path=/; SameSite=Lax · 3 __Host-sid=a3; Secure; Path=/; Domain=app.example.com · 4 __Host-sid=a4; Secure; Path=/account · 5 __Secure-theme=dark; Secure; Path=/account; Domain=example.com · 6 __Secure-theme=dark; Path=/ · 7 __host-sid=a7; Path=/ · 8 theme=dark; Path=/; Domain=example.com

先写下每种前缀要求的条件清单,再逐行对照。__Host- 有三条,__Secure- 有一条。也请在规范中确认浏览器对前缀的比对是否区分大小写。以 # 开头的行可以当作注释。

一次性证明完整流程

用 /root/st/cookie/e2e.sh 依次测试登录、带 Cookie 访问 /me、不带 Cookie 访问 /me、错误密码、退出登录、重放旧 Cookie,并统计退出登录后存储中剩余的 __Host-sid 行数,将 login=200 me=200 no_cookie=401 bad_password=401 logout=200 replay=401 jar_after_logout=0 写成一行,保存到 /root/st/cookie/e2e.out。

把前面各步的确认串联起来,用同一个 Cookie 存储运行即可。用于重放测试的旧值必须在退出登录之前从存储中取出。