签发 __Host- 会话 Cookie 并正确删除
目标
在 127.0.0.1:8301 上搭建登录服务器,签发、检查并撤销 __Host-sid 会话 Cookie,并确认该 Cookie 确实被浏览器(curl)的存储接受并删除。
为什么重要
Cookie 问题从服务器日志里看不出来。服务器正常发出了 Set-Cookie,浏览器却因前缀规则或 Secure 条件悄悄丢弃,症状就只表现为“偶尔登录不了”。所以本实验在看响应头的同时,还要看 curl 的 Cookie 存储。curl 8.5 也不会保存违反 __Host-、__Secure- 规则的 Cookie,因此存储中是否留下,就近似于是否被接受。退出登录也用同样的眼光来看。删除 Cookie 只是对浏览器的请求,对于复制了该值的人来说,只有服务器记录是否被删除才有意义。
步骤
- 在 127.0.0.1:8301 上启动
/root/st/cookie/app.py。GET /healthz返回 200 和{"ok":true}。 - 向
POST /login发送 JSON{"user":"alice","password":"wonderland"},返回 200 和{"user":"alice"},并签发__Host-sidCookie。值是用secrets生成的至少 128 位随机数(base64url 至少 22 个字符),每次登录都必须不同,且不能包含用户名。bob/builder也能登录。 - 给会话 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。 GET /me在存在有效的__Host-sid时返回 200 和{"user":"<사용자>"}(占位符为用户名);没有 Cookie,或值不是服务器签发过的值时,返回 401。- 错误密码(
alice/wrongpass)和不存在的用户(mallory)调用POST /login,都返回 401,且一行Set-Cookie都不能有。 POST /logout返回 200 并删除服务器端会话。响应中必须有删除__Host-sid的Set-Cookie(Max-Age=0),而且 curl 存储必须真正接受该删除,使 Cookie 消失。用退出登录之前复制下来的 Cookie 值调用GET /me,必须返回 401。第 3 步的jar.txt不要覆盖,改用其他存储文件来测试。- 当
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
- 1
- 用
/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。
参考
- 查看响应头:
curl -s -D - -o /dev/null ...保留、写入存储:curl -c <파일> -b <파일> ...(占位符为文件名) - curl 存储文件是用制表符分隔的七列。可以用
awk -F'\t'取出各列。 - 本 Pod 中的 curl 8.5 即使对通过
http://127.0.0.1收到的 Secure Cookie 也会保存并回传。真实浏览器需要 HTTPS(localhost 除外)。 - 常见错误 1:退出登录时只删除 Cookie 而保留服务器会话——复制下来的值仍然有效。
- 常见错误 2:删除用的 Cookie 漏掉
Secure或Path=/——会触犯__Host-规则,删除本身被忽略,旧 Cookie 保留。
启动登录服务器
在 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 存储运行即可。用于重放测试的旧值必须在退出登录之前从存储中取出。