主题
Cookie、Session 与身份认证:登录到底发生了什么
上一课我们说"服务端不关心请求是谁发的"——那网站怎么知道你是你? 这节课拆开登录这件事。你在 www.myxbw.cn 点一下"登录"按钮之后发生的一连串动作,是所有越权、CSRF、会话劫持类漏洞的地基。
一、HTTP 是无状态的
HTTP 协议有个刻意的设计:它不记人。第一条请求和第一百条请求之间,服务器在协议层面看不出它们来自同一个人。
这带来一个矛盾:
- 协议层:每个请求都是"陌生人第一次上门";
- 业务层:我需要知道"这个人是刚才登录过的张三"。
解决思路只有一条:让客户端每次请求都主动出示一张凭证。这就是 Cookie 存在的全部理由。
二、Cookie 是什么
Cookie 就是浏览器替某个域名保存的一小段键值文本,并在之后访问该域名时自动附加到请求头里。
一次典型的往返:
http
# 第一次:服务器要求浏览器存东西
HTTP/1.1 200 OK
Set-Cookie: theme=dark; Path=/; Max-Age=604800
# 此后浏览器每次访问该域名,都会自动带上
GET / HTTP/1.1
Host: www.myxbw.cn
Cookie: theme=dark注意关键词:自动。浏览器不需要你的代码提醒它,只要域名匹配、路径匹配、没过期、没被 SameSite 拦下,它就会带。这个"自动"既是便利,也是 CSRF 能够成立的根源。
三、Set-Cookie 的每个属性及安全含义
服务器下发 Cookie 时可以附带一堆属性,它们决定了这个 Cookie 有多安全。
| 属性 | 作用 | 安全含义 |
|---|---|---|
HttpOnly | 禁止 JavaScript 通过 document.cookie 读取 | 防 XSS 偷 Cookie 的第一道墙;会话 Cookie 必须加 |
Secure | 只在 HTTPS 连接上发送 | 防止明文 HTTP 或中间人窃听;没它的话,一次 http 跳转就漏了 |
SameSite=Strict | 任何跨站请求都不带 | 最安全,但用户从外链点进来会显示未登录 |
SameSite=Lax | 跨站的顶级导航(点链接)带,跨站子请求(图片/表单/iframe)不带 | 现代浏览器默认值,能挡掉大部分 CSRF |
SameSite=None | 总是带,必须同时有 Secure | 跨站场景(嵌入式 iframe)才需要,等于主动关掉 CSRF 防护 |
Domain | 允许哪些子域共享 | 设为 .myxbw.cn 后,任何一个子域被攻破都可能波及主站 |
Path | 限定生效路径 | 只是"发送范围",不是隔离手段,不要指望它保护安全 |
Expires / Max-Age | 过期时间 | 会话凭证越短命越安全;没有它就活到浏览器关闭 |
一个"教科书级别"的会话 Cookie 长这样:
http
Set-Cookie: SESSIONID=a1b2c3d4; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800对照检查一下:不能 JS 读(HttpOnly)、只能走加密(Secure)、跨站子请求不带(SameSite=Lax)、半小时就过期。
四、Session 与 Session ID
Cookie 本身是明文存在用户电脑上的,所以绝不能把"我是管理员"这种信息直接存进去——用户改一改就提权了。
于是有了 Session:Cookie 里只放一个随机编号,真正的数据放服务端。
text
浏览器手里:SESSIONID=a1b2c3d4
服务器手里(内存/Redis/数据库里):
a1b2c3d4 → { user_id: 42, username: "张三", role: "user", login_time: ... }这样设计的三个好处:
- 用户看不到也改不了自己的角色(改的只是一个编号);
- 服务端可以随时作废某个 Session(退出登录);
- 敏感信息不出服务端。
两个经典攻击方式,先建立概念:
- Session 固定(Session Fixation):攻击者先拿到一个 Session ID,想办法让受害者的浏览器使用同一个 ID;等受害者用它登录后,这个 ID 在服务端就变成了"已登录"状态,攻击者直接用同一个 ID 就进去了。防御:登录成功后必须重新生成 Session ID。
- Session 劫持(Session Hijacking):直接偷走 Session ID(XSS、抓包、日志泄露)。因为服务端只认 ID 不认人,拿到 ID 就等于拿到身份。防御:
HttpOnly+Secure+ 短有效期 + 登录后轮换 ID。
五、Token 与 JWT 简述
另一种思路是不存服务端:把身份信息本身签名后交给客户端。JWT(JSON Web Token)就是这种。
结构是三段用 . 分隔的 Base64 文本:
text
eyJhbGciOiJIUzI1NiJ9 . eyJ1c2VyX2lkIjo0Miwicm9sZSI6InVzZXIifQ . 签名部分
头部 载荷(明文可解) 签名必须记住的三条:
- 签名不等于加密。中间那段载荷只是 Base64 编码,任何人粘到 jwt.io 就能读出来。绝对不要往里塞密码、身份证号、密钥。
- 签名保证的是完整性——别人改不了内容,改了签名就对不上。它不保证机密性。
- 常见的坑是"算法混淆":把头部
alg改成none,骗过没做校验的服务端。这是 第四阶段 里用 Burp Suite 改包的经典练习。
六、登录流程逐步拆解
把一次完整登录拆成五步:
text
① 提交表单 POST /login body: username=alice&password=xxx
② 服务端校验 取出用户记录 → 用哈希算法比对密码 → 判断是否通过
③ 下发凭证 Set-Cookie: SESSIONID=随机值; HttpOnly; Secure; SameSite=Lax
④ 后续请求 GET /profile Cookie: SESSIONID=随机值 → 服务端查表还原身份
⑤ 退出登录 POST /logout → 服务端删除该 Session → Set-Cookie 置空并设 Max-Age=0第 ② 步里有两个常被做错的地方:密码必须加盐哈希存储(通常用 bcrypt/Argon2),绝不能存明文或裸 MD5;登录失败的信息必须统一(不能一个说"用户名不存在"、一个说"密码错误",那是用户名枚举漏洞)。
第 ⑤ 步也常被做成"假退出":只让浏览器删 Cookie,服务端那条 Session 还活着——攻击者手里的旧 Session ID 依然能用。真正的退出必须在服务端销毁 Session。
七、和后续漏洞的关系
这一节是本课的落点。你以后要读的那些文章,几乎都能在这里找到源头:
| 后续漏洞 | 与本课的关系 |
|---|---|
| CSRF(跨站请求伪造) | 依赖 Cookie 自动携带:你登录了银行站,另一个恶意页面偷偷向银行发一个表单请求,浏览器自动带上你的 Cookie,银行以为是你本人在操作。SameSite 和 CSRF Token 就是为此而生 |
| XSS(跨站脚本) | 能偷非 HttpOnly 的 Cookie。所以加 HttpOnly 不能阻止 XSS 本身,但能让攻击者拿不到会话 |
| 越权 / 水平权限 | 服务端只检查了"你登录了吗",没检查"这条数据是你的吗"。Session 里存着 user_id=42,代码却用 URL 里的 ?id=43 去查数据,于是你看到了别人的订单 |
| 会话固定 | 登录前后 Session ID 不轮换,攻击者可以提前"预定"你的身份 |
先记住一句话:凭证是身份,身份决定权限;这三样任何一环被绕过,都是一次"我就是别人"的攻击。
💡 动手实验 1:用 F12 观察真实站点的 Cookie
💡 动手实验 1:打开
https://www.myxbw.cn,按F12→ 切到 Application(应用) 面板 → 左侧找到 Cookies → 展开https://www.myxbw.cn。 你会看到一张表,每行是一个 Cookie,每个 Cookie 都能看到 HttpOnly / Secure / SameSite / Expires 这几列的勾选情况。 如果你的网站目前只返回了一条theme之类的 Cookie,就再打开任意一个你常用的登录态网站对比看看——登录类站点的会话 Cookie,HttpOnly几乎一定是打勾的。
继续做对比观察:
text
观察点 1:会话 Cookie 那一行,Secure 打勾了吗?(打勾 = 只在 HTTPS 发送)
观察点 2:SameSite 列显示的是什么?(Lax / Strict / None)
观察点 3:在 Console 里敲 document.cookie,看能读出哪些、读不出哪些💡 动手实验 1 关键一步:在 Console 里执行
document.cookie。你会看到只有没打HttpOnly勾的那些被打印出来——这就是"XSS 能偷哪些 Cookie"的直观答案,也是为什么会话 Cookie 必须加HttpOnly。
💡 动手实验 2:用 curl 手动带 Cookie 发请求
💡 动手实验 2:先在你自己的本地服务上造一个会下发 Cookie 的接口(用上一课 服务端如何接收请求 里那个 Node 服务改几行即可)。 然后用 curl 走一遍"第一次拿 Cookie → 第二次带着 Cookie 去"的完整流程,亲眼看见 Cookie 是浏览器之外的普通请求头。
bash
# 第一步:让服务器下发 Cookie,并把收到的 Cookie 存到本地文件 cookies.txt
curl -c cookies.txt -v "http://127.0.0.1:3000/login?user=alice"
# 第二步:读取文件里保存的 Cookie,自动附加到下一次请求
curl -b cookies.txt -v "http://127.0.0.1:3000/profile"
# 第三步:不信邪,干脆手工把 Cookie 写死在请求头里——效果完全一样
curl -v "http://127.0.0.1:3000/profile" -H "Cookie: SESSIONID=a1b2c3d4"
# 第四步:把 SESSIONID 随便改几个字符,观察服务器是不是把你当成陌生人
curl -v "http://127.0.0.1:3000/profile" -H "Cookie: SESSIONID=ffffffff"⚠️ 第三步的"手工带上一个别人不知道的 SESSIONID"看起来像在伪造身份,但只在
127.0.0.1上你自己写的这个练习服务里做。任何真实站点都必须先拿到明确书面授权,边界问题写在 四、工具与靶场 的合规课里。
💡 动手实验 2 进阶:把
cookies.txt用记事本打开看一眼。你会发现里面只是纯文本——这解释了为什么"Cookie 存在用户电脑上"意味着"用户可以随便看、随便改",也解释了为什么真正重要的东西必须放服务端 Session,而不是放 Cookie。
bash
# 服务端视角:打印一下收到的 Cookie 头(在你的 Node 服务里加一行)
console.log('收到 Cookie:', req.headers.cookie || '(无)');小结
- HTTP 无状态,所以需要 Cookie 这张"随身凭证";浏览器会自动携带它,这正是 CSRF 的前提。
Set-Cookie的每个属性都有明确的安全含义,其中HttpOnly、Secure、SameSite三个是会话 Cookie 的底线。- Session 的本质是"Cookie 里只放编号,数据放服务端";Session 固定和劫持都能让攻击者变成你。
- JWT 的载荷是明文可读的,签名只保证完整性,不保证机密性。
- 登录是这五步:提交 → 校验 → 下发 → 携带 → 销毁;密码加盐哈希、登录后轮换 Session ID、退出时服务端销毁,三件都不能省。
- 越权、CSRF、XSS 偷 Cookie,全部都建立在"身份怎么被记住"这件事上。
身份讲完了,接下来看身份背后存放的东西:数据库。下一课 SQL 基础:从增删改查到注入的起点,你会第一次见到 ' OR '1'='1。
📎 本课对应知识库阅读:《知识库里的 XSS 与 CSRF 主题文章》