【Linux网络编程】5-1 加餐 · HTTP Cookie 与 Session(从"无状态"到"记住你")
摘要:HTTP 是无状态 协议,服务器天生不记得"你是谁"。本篇从 B 站"刷新后还认得我"这个现象出发,讲清 Cookie 的
Set-Cookie/Cookie一对报文头如何让浏览器替你把身份带上 ,expires/path/domain/secure/HttpOnly各管什么,然后顺着"Cookie 存在客户端不安全"这条线引出 Session,给出Session/SessionManager/ 请求解析 / 业务分发的完整落地代码,并补充 SameSite、CSRF、Session 固定攻击、分布式 Session 等必须掌握的内容。关键词:Cookie、Set-Cookie、Session、Session ID、HttpOnly、SameSite、RFC1123、CSRF、分布式 Session
目录
- [一、问题导入:HTTP 记不住你](#一、问题导入:HTTP 记不住你)
- 二、Cookie:把身份存在浏览器里
- [三、Set-Cookie 详解(重点)](#三、Set-Cookie 详解(重点))
- [四、代码实战:给自己的 HTTP 服务器加 Cookie](#四、代码实战:给自己的 HTTP 服务器加 Cookie)
- [五、Cookie 单独用有什么问题](#五、Cookie 单独用有什么问题)
- 六、Session:把身份存在服务器里
- [七、代码实战:Session 落地](#七、代码实战:Session 落地)
- 八、实验验证:两个浏览器
- 九、必须掌握的扩展
- 十、总结
- [附录:favicon.ico 是什么](#附录:favicon.ico 是什么)
一、问题导入:HTTP 记不住你
先看一个所有人都经历过的场景:
未登录打开 B 站 → 点"登录" → 扫码 → 登录成功
之后刷新任何页面 → 右上角还是我的头像
第二天再打开 → 仍然是登录态
课件提了两个问题:
- B 站是怎么认识我这个登录用户的?
- HTTP 明明是无状态、无连接的,怎么能够"记住"我?
答案的关键在于:HTTP 协议本身确实什么都没记。 服务器收到第二个请求时,除了 IP 和端口,它对你一无所知。所以"记住"这件事,必须靠额外的机制:让浏览器每次请求都主动带上一个"身份证"。
这个"身份证"就是 Cookie ,而"身份证对应的档案"就是 Session。
Cookie → 存在客户端(浏览器) → 解决"怎么把身份带过去"
Session → 存在服务器端 → 解决"身份对应哪些数据"
二、Cookie:把身份存在浏览器里
2.1 定义
HTTP Cookie (Web Cookie / 浏览器 Cookie,简称 Cookie)是服务器发送到用户浏览器、并保存在浏览器上的一小块数据 。浏览器之后向同一服务器再次发起请求时,会自动把它携带上并发送回服务器。
它的典型用途是告知服务端两个请求是否来自同一浏览器,比如保持登录状态、记录用户偏好。
2.2 工作原理(三步,必须背下来)
① 第一次访问:服务器在【响应头】里写 Set-Cookie,Cookie 随响应下发
Browser ──GET /──────────────────────────────▶ Server
Browser ◀──HTTP/1.1 200 OK──────────────────── Server
Set-Cookie: username=zhangsan; path=/
② 浏览器把 Cookie 按【域名】存到本地
③ 之后每次请求:浏览器自动在【请求头】里带上 Cookie
Browser ──GET / Cookie: username=zhangsan────▶ Server
三个要点:响应头下发 → 按域名本地保存 → 请求头自动带回。整个过程浏览器全自动,前端一行代码都不用写。
2.3 两种生命周期
| 类型 | 特征 | 存活时间 |
|---|---|---|
| 会话 Cookie(Session Cookie) | 没设 expires / Max-Age |
浏览器关闭即失效(注意:现代浏览器"恢复上次会话"可能会保留) |
| 持久 Cookie(Persistent Cookie) | 设了明确的过期时间 | 跨多次浏览器会话存在,直到过期 |
持久 Cookie 本质上是浏览器特定目录下的一个文件,通常以二进制或 SQLite 格式存储,直接打开是乱码。想看就在浏览器里看:
- Chrome 设置页:
chrome://settings/cookies- 开发者工具:
F12→Application→Storage→Cookies
2.4 安全性 & 用途
- 风险 :Cookie 存在客户端,存在被篡改或窃取的风险(用户自己也能改)。
- 用途 (按重要性排序):
- 用户认证和会话管理(最重要)
- 跟踪用户行为(广告、统计)
- 缓存用户偏好(主题、语言)
三、Set-Cookie 详解(重点)
3.1 一对报文头
这是最容易混淆的点,一定要分清方向:
| 报文头 | 出现位置 | 方向 | 谁写 | 一条能带几个 |
|---|---|---|---|---|
Set-Cookie |
响应头 | 服务器 → 浏览器 | 我们自己 AddHeader() |
可以多行,一行一个 Cookie |
Cookie |
请求头 | 浏览器 → 服务器 | 浏览器自动填 | 通常只有一行 ,多个键值用 ; 分隔 |
http
HTTP/1.1 200 OK
Set-Cookie: username=zhangsan; path=/
Set-Cookie: sessionid=8a3f...; HttpOnly ← 多个 Set-Cookie 就是多行
http
GET /index.html HTTP/1.1
Host: 192.168.109.128:8888
Cookie: username=zhangsan; sessionid=8a3f... ← 浏览器合并成一行
3.2 基本格式
Set-Cookie: <name>=<value>; <属性1>; <属性2> ...
完整示例:
http
Set-Cookie: username=peter; expires=Thu, 18 Dec 2025 12:00:00 GMT; path=/; domain=.example.com; secure; HttpOnly
3.3 属性逐个拆
| 属性 | 值示例 | 作用 | 掌握程度 |
|---|---|---|---|
name=value |
username=peter |
Cookie 名和值 | 必会 |
expires |
Thu, 01 Jan 2030 12:00:00 GMT |
绝对过期时间,格式必须是 RFC1123 | 必会 |
Max-Age |
3600 |
相对 过期秒数,优先级高于 expires ;0 表示立即删除 |
必会(课件未提,高频) |
path |
/a/b |
只有该路径前缀的请求才会带这个 Cookie,默认是设置它的路径 | 必会 |
domain |
.example.com |
哪些主机可以接受,点前缀表示包含所有子域名,默认只本主机 | 了解 |
secure |
无值 | 仅 HTTPS 才发送,防止被明文 HTTP 截获 | 必会 |
HttpOnly |
无值 | 禁止 JS 通过 document.cookie 读取,防 XSS 偷 Cookie |
必会 |
SameSite |
Lax / Strict / None |
跨站请求是否携带,防 CSRF ;None 必须同时加 secure |
必会(课件未提,高频) |
写法注意 :属性之间用 分号 + 空格 分隔;name 和 value 之间用 =;值里若含空格、分号、逗号等特殊字符,必须做 URL 编码。
3.4 expires 的时间格式(RFC1123)
Tue, 01 Jan 2030 12:34:56 GMT
└┬┘ └┬┘ └┬┘ └┬┘ └───┬───┘ └┬┘
星期 日 月 年 时分秒 时区
- 星期、月份用英文三字母缩写 (
Sun``Sat、Jan``Dec) - 日、时、分、秒两位数补零
- 时区写
GMT或UTC
GMT vs UTC (课件占了两页,这里一句话收掉):GMT 基于地球自转,UTC 基于原子钟。二者在网络场景中差值远小于 1 秒,可以混用 ,浏览器都能解析。写
GMT更符合 RFC1123 原文,写UTC也被广泛接受。记住结论就够了,不用背历史。
3.5 Cookie 的几个硬限制(必须知道)
| 限制 | 数值 | 说明 |
|---|---|---|
| 单个 Cookie 大小 | 约 4KB | 超了浏览器直接丢弃,所以不能塞大对象 |
| 单域名 Cookie 数量 | 约 50 个(因浏览器而异) | 超了会按策略淘汰旧的 |
| 域名隔离 | 严格 | a.com 的 Cookie 不会发给 b.com(同源策略的一部分) |
| 端口不隔离 | ------ | 同一主机不同端口的 Cookie 会共享,这点和同源策略不同 |
四、代码实战:给自己的 HTTP 服务器加 Cookie
4.1 文件结构
Comm.hpp HttpProtocol.hpp InetAddr.hpp LockGuard.hpp Log.hpp
Main.cc Makefile Socket.hpp TcpServer.hpp Thread.hpp ThreadPool.hpp
(就是前面那套自研 HTTP 服务器,只改 HttpProtocol.hpp / 业务层,不用动 Socket 层------因为 Cookie 只是多了一个报文头而已。)
4.2 先证明"能写进去"
最小改动:在响应里加一行 Set-Cookie。
cpp
std::string ProveCookieWrite()
{
return "Set-Cookie: username=zhangsan;";
}
std::string HandlerHttp(std::string request)
{
HttpRequest req;
req.Deserialize(request);
req.DebugHttp();
HttpResponse resp;
resp.SetCode(200);
resp.SetDesc("OK");
resp.AddHeader("Content-Type: text/html");
resp.AddHeader(ProveCookieWrite()); // ← 加这一行
resp.AddContent("<html><h1>helloworld</h1></html>");
return resp.Serialize();
}
验证方式:浏览器访问一次 → F12 → Application → Cookies,能看到 username=zhangsan。
4.3 再证明"会自动提交"
什么都不用改,直接刷新页面 。然后看服务器的 DebugHttp() 打印:
_req_line: GET / HTTP/1.1
---> Host: 192.168.109.128:8888
---> Cookie: username=zhangsan ← 浏览器自己加上的
---> User-Agent: ...
这就是 Cookie 的全部魔法:你只写过一次
Set-Cookie,之后每一次请求浏览器都替你带。 而且注意------浏览器为了拿网站图标还会额外发一个favicon.ico的请求(见附录),那个请求同样带着 Cookie,所以服务端日志里会看到两条。
4.4 过期时间:手写 RFC1123(核心代码)
expires 没有现成 API 一把梭,得自己拼字符串:
cpp
static std::string GetMonthName(int month)
{
static const std::vector<std::string> months =
{"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
return months[month]; // tm_mon 是 0~11,正好直接下标
}
static std::string GetWeekDayName(int day)
{
static const std::vector<std::string> weekdays =
{"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
return weekdays[day]; // tm_wday 是 0~6
}
// 生成"从现在起 t 秒之后"的 RFC1123 时间串
std::string ExpireTimeUseRfc1123(int t)
{
time_t timeout = time(nullptr) + t;
struct tm *tm = gmtime(&timeout); // ★ 必须 gmtime,不能用 localtime
char timebuffer[1024];
snprintf(timebuffer, sizeof(timebuffer),
"%s, %02d %s %d %02d:%02d:%02d UTC",
GetWeekDayName(tm->tm_wday).c_str(),
tm->tm_mday,
GetMonthName(tm->tm_mon).c_str(),
tm->tm_year + 1900, // ★ tm_year 是"从 1900 起的年数"
tm->tm_hour, tm->tm_min, tm->tm_sec);
return timebuffer;
}
三个必踩的坑,全在这 20 行里:
| 坑 | 现象 | 原因 |
|---|---|---|
用 localtime 代替 gmtime |
东八区机器上 Cookie 早了/晚了 8 小时 | localtime 带本地时区偏移,而 Cookie 时间必须是 UTC |
忘记 tm_year + 1900 |
年份变成 126 |
tm_year 存的是"距 1900 年的偏移" |
忘记 tm_mon 从 0 开始 |
月份全部错一位(12 月越界崩溃) | tm_mon 范围是 0~11 |
用法:
cpp
std::string ProveCookieTimeOut()
{
// 让 cookie 1 分钟后过期
return "Set-Cookie: username=zhangsan; expires=" + ExpireTimeUseRfc1123(60) + ";";
}
4.5 验证 path 作用域
cpp
std::string ProvePath()
{
return "Set-Cookie: username=zhangsan; path=/a/b;";
}
设置后分别访问:
| 访问 URL | 请求头是否带 Cookie | 原因 |
|---|---|---|
http://8.137.19.140:8888/a/b |
✅ 带 | 完全命中 path |
http://8.137.19.140:8888/a/b/x |
✅ 带 | 是 /a/b 的子路径 |
http://8.137.19.140:8888/a/x |
❌ 不带 | 只是前缀像,不是子路径 |
http://8.137.19.140:8888/ |
❌ 不带 | 更上层路径不在作用域内 |
http://8.137.19.140:8888/x/y |
❌ 不带 | 完全不同路径 |
结论:path 是"前缀 + 路径段边界"匹配,不是纯字符串前缀。 /a/b 匹配 /a/b/x,但不匹配 /a/bc。
这也解释了为什么真实项目里
path=/最常见------不然你每加一个接口都要重新下发 Cookie。
五、Cookie 单独用有什么问题
Cookie 能用,但一旦往里面放私密数据就出事了:
- 我们测试写的是
username=zhangsan,无所谓; - 真实场景要写的是用户名密码、身份标识、浏览痕迹呢?
- 本质问题:私密数据存在用户端(浏览器),天生可被篡改、可被窃取。
- 篡改:用户打开 F12 就能改
username=admin,服务器如果无脑信任就完了; - 窃取:XSS 注入一段 JS 读
document.cookie,或者中间人抓明文 HTTP。
- 篡改:用户打开 F12 就能改
所以正确的姿势是:Cookie 里只放一个"毫无意义的随机串",真正的数据放服务器。 这就是 Session。
六、Session:把身份存在服务器里
6.1 定义
HTTP Session 是服务器用来跟踪用户与服务器交互期间用户状态的机制。因为 HTTP 无状态(每个请求独立),服务器需要 Session 来记住用户信息。
6.2 工作原理(三步)
① 首次访问 /login:服务器创建唯一 Session ID,
把用户数据存进自己的 Session 表,然后用 Cookie 把 ID 发给浏览器
Server: sessions["8a3f9c..."] = {username:"user-0", status:"logined"}
──▶ Set-Cookie: sessionid=8a3f9c...; HttpOnly
② 之后每个请求:浏览器自动带 Cookie: sessionid=8a3f9c...
③ 服务器拿 sessionid 去表里查 → 命中就"认出"你是谁
关键理解:Session 依然要靠 Cookie 传递 Session ID。 所以两者不是二选一,而是搭档。Cookie 是"车",Session 是"货"。
6.3 安全性
| 风险 | 说明 | 对策 |
|---|---|---|
| Session ID 被窃取 | ID 在客户端和服务器之间传递,同样可能被抓包/被 XSS 偷 | HttpOnly + secure + HTTPS |
| 泄露范围 | 但比 Cookie 好很多:Cookie 丢了泄露的是全部私密数据;Session ID 丢了只泄露一个"门禁卡",且可被服务端随时作废 | 服务端可主动失效 |
| Session 固定攻击 | 攻击者预先塞给你一个他知道的 ID,你登录后这个 ID 就成了有效凭证 | 登录成功后必须重新生成 Session ID |
课件那句话值得记住:"一般虽然 Cookie 被盗取了,但用户只泄漏了一个 Session ID,私密信息暂时没有被泄露的风险。" 这就是 Session 相对裸 Cookie 的核心价值------把"凭证"和"数据"分离。
另外,Session ID 天然便于服务端做客户端有效性管理,比如:异地登录检测、强制下线、单点登录踢人。
6.4 超时与失效
- 超时:Session 可设过期时间,超过自动失效(内存版一般配合定时清理或用"最后访问时间 + 过期阈值"懒删除)。
- 主动失效:用户登出时,服务端直接删掉这条 Session。
6.5 存储位置
| 位置 | 特点 | 适用 |
|---|---|---|
进程内存(unordered_map) |
最快,但进程重启就全丢、多进程不共享 | 学习 / 单机 demo |
| 数据库(MySQL) | 持久、可共享,但每次请求都查库,慢 | 低频场景 |
| 缓存(Redis / Memcached) | 快 + 可共享 + 自带 TTL | 生产主流 |
6.6 用途
- 用户认证和会话管理
- 存储用户的临时数据(购物车是经典例子)
- 实现分布式系统的会话共享(Session 数据放共享缓存/数据库)
七、代码实战:Session 落地
新增一个 Session.hpp,其余文件不变(业务层多一个成员)。
7.1 Session:一条"档案"
cpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <ctime>
#include <unistd.h>
#include <unordered_map>
// 用来进行测试说明:真实项目里这里会有 userId、权限、购物车......
class Session
{
public:
Session(const std::string &username, const std::string &status)
: _username(username), _status(status)
{
_create_time = time(nullptr); // 先存时间戳,需要展示时再转换
}
~Session() {}
public:
std::string _username;
std::string _status;
uint64_t _create_time;
};
7.2 SessionManager:一张"档案柜"
cpp
using session_ptr = std::shared_ptr<Session>;
class SessionManager
{
public:
SessionManager() { srand(time(nullptr) ^ getpid()); }
std::string AddSession(session_ptr s)
{
// 随机数 + 时间戳(仅测试用!真实项目见下方"必读")
uint32_t randomid = rand() + time(nullptr);
std::string sessionid = std::to_string(randomid);
_sessions.insert(std::make_pair(sessionid, s));
return sessionid;
}
session_ptr GetSession(const std::string &sessionid)
{
auto it = _sessions.find(sessionid);
if (it == _sessions.end()) return nullptr; // 查不到返回空
return it->second;
}
private:
std::unordered_map<std::string, session_ptr> _sessions;
};
必读:课件的 ID 生成方式不能上生产。
rand() + time()熵极低:rand()未播种时可预测,且同一秒内碰撞概率极高,攻击者也能暴力枚举。生产应使用:
boost::uuids::random_generator(课件注释里也提到了 boost uuid 库)/dev/urandom读 16 字节,转 hex(Linux 下最省事)openssl的RAND_bytes()要求:至少 128 bit 随机、全局唯一、不可预测。
另外,
unordered_map在多线程下不是线程安全的 (你服务器是线程池),生产版要给AddSession/GetSession加锁,换成分片锁或直接用 Redis。
7.3 请求侧:从 Cookie 头里抠出 sessionid
在 HttpRequest 里加两个成员和一段解析:
cpp
std::vector<std::string> _cookies; // 其实 cookie 可以有多个
std::string _sessionid; // 请求携带的 sessionid
void Parse()
{
// 1. 解析请求行
std::stringstream ss(_req_line);
ss >> _method >> _url >> _http_version;
// 2. 找到 Cookie 头
std::string prefix = "Cookie: ";
for (auto &line : _req_header)
{
if (strncmp(line.c_str(), prefix.c_str(), prefix.size()) == 0)
{
_cookies.emplace_back(line.substr(prefix.size()));
break;
}
}
// 3. 从 cookie 串里找 sessionid=
prefix = "sessionid=";
for (const auto &cookie : _cookies)
{
if (strncmp(cookie.c_str(), prefix.c_str(), prefix.size()) == 0)
_sessionid = cookie.substr(prefix.size());
}
}
这里有个隐藏 bug,值得单独说:
浏览器会把所有 Cookie 合并成一行 :
Cookie: username=zhangsan; sessionid=8a3f...。上面这段代码直接
strncmp(cookie.c_str(), "sessionid=", ...),只有当 sessionid 恰好排在第一 时才匹配得上。如果浏览器先发了username=...,sessionid 就永远解析不到。正确做法是先按
"; "切分,再逐个键值对比较:
cpp// 正确版:先按 "; " 拆,再逐个匹配 key std::string raw = _cookies[0]; // 例:"username=z; sessionid=8a3f" std::vector<std::string> kv; size_t pos; while ((pos = raw.find("; ")) != std::string::npos) { kv.push_back(raw.substr(0, pos)); raw.erase(0, pos + 2); } kv.push_back(raw); // 最后一个 for (const auto &item : kv) if (item.rfind("sessionid=", 0) == 0) // 只比较前缀,不用 strncmp _sessionid = item.substr(10); // "sessionid=" 正好 10 字节这也顺带解决了"Cookie 值里含
="的问题(只按第一个=切)。
7.4 业务侧:/login 与其他路径
cpp
std::string ProveSession(const std::string &session_id)
{
return "Set-Cookie: sessionid=" + session_id + ";";
}
std::string HandlerHttp(std::string request)
{
HttpRequest req;
HttpResponse resp;
req.Deserialize(request);
req.Parse(); // ← 新增:解析 Cookie / sessionid
static int number = 0; // 仅测试用
if (req.Url() == "/login") // 模拟登录入口
{
std::string sessionid = req.SessionId();
if (sessionid.empty()) // 历史没登录过 → 新建会话
{
std::string user = "user-" + std::to_string(number++);
session_ptr s = std::make_shared<Session>(user, "logined");
std::string sid = _session_manager->AddSession(s);
lg.LogMessage(Debug, "%s 被添加, sessionid是: %s\n", user.c_str(), sid.c_str());
resp.AddHeader(ProveSession(sid)); // 下发 sessionid
}
}
else // 其他路径:判断"谁还活跃"
{
std::string sessionid = req.SessionId();
if (!sessionid.empty())
{
session_ptr s = _session_manager->GetSession(sessionid);
if (s != nullptr)
lg.LogMessage(Debug, "%s 正在活跃.\n", s->_username.c_str());
else
lg.LogMessage(Debug, "cookie : %s 已经过期, 需要清理\n", sessionid.c_str());
}
}
resp.SetCode(200);
resp.SetDesc("OK");
resp.AddHeader("Content-Type: text/html");
resp.AddContent("<html><h1>helloworld</h1></html>");
return resp.Serialize();
}
7.5 第二坑:服务器重启后浏览器还留着旧 sessionid
课件专门用注释标了:
cpp
// 这个地方有坑,一定要判断服务器端 session 对象是否存在,
// 因为可能测试的时候浏览器还有历史 sessionid,但服务器重启之后 session 对象没有了
if (s != nullptr) { ... }
else { /* 应当告诉浏览器清掉这个失效 cookie */ }
原因很直白:Session 在服务器内存里,进程一重启就没了;但 Cookie 在浏览器硬盘里,活得好好的。 于是下一个请求会带着一个"服务器不认识"的 ID 过来。
生产做法:
cpp
// 优雅处理:回一个 Max-Age=0 让浏览器删掉这个 cookie,并重新登录
resp.AddHeader("Set-Cookie: sessionid=deleted; Max-Age=0; path=/");
顺带一提:这不是 bug,而是内存型 Session 的固有特性。 想跨重启保留,就得把 Session 落到 Redis / DB 里(见 9.4)。
八、实验验证:两个浏览器
想看"服务器真的能区分不同人",就模拟两个用户:
准备:Chrome + Edge(或两个不同浏览器 / 一个浏览器正常窗口 + 一个无痕窗口)。
-
清掉两个浏览器里该站点的所有 Cookie
- 历史没做过测试可以跳过;
- Chrome 尤其要清 :它可能缓存了历史
sessionid,导致你一访问就带着旧 ID,看不出"新建会话"的效果。打印一下请求头就能明白为什么要删。
-
两个浏览器分别访问
/login- 各自拿到一个不同的 sessionid;
- 服务端日志出现两条
user-0 被添加/user-1 被添加。
-
两个浏览器再访问任意站点资源
- 浏览器自动带各自的
sessionid; - 服务端日志分别打印
user-0 正在活跃/user-1 正在活跃。
- 浏览器自动带各自的
结论:服务器已经能识别"这是哪个浏览器"了。 这就是登录态的最小闭环。
小技巧:观察日志时会看到
favicon.ico的请求也带着 Cookie,也是"活跃"的------不用管它(见附录)。
九、必须掌握的扩展
9.1 Cookie vs Session 一张表
| 维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端(浏览器,可落盘) | 服务端(内存 / Redis / DB) |
| 存什么 | 小块数据或一个 ID | 用户的真实状态数据 |
| 容量 | 单个约 4KB,数量有限 | 只受服务器内存限制 |
| 安全性 | 低:可被用户查看、篡改、XSS 窃取 | 较高:数据不落地客户端,ID 可随时作废 |
| 服务器压力 | 无 | 有(占内存,分布式要共享) |
| 跨域 | 受 domain 限制 | 不涉及(只靠 ID 关联) |
| 依赖关系 | 可独立使用 | 通常依赖 Cookie 传 ID |
| 失效控制 | 靠浏览器(用户可删) | 服务端说了算(可强制下线) |
一句话记忆:Cookie 是"身份证",Session 是"户籍档案"。身份证可以被伪造,但档案只在公安局(服务器)里。
9.2 如果浏览器禁用了 Cookie,Session 还能用吗?
依然可以用,只是ID 的传递方式要换:
| 方案 | 做法 | 缺点 |
|---|---|---|
| URL 重写 | 把 ;jsessionid=xxx 拼进 URL 或 ?sid=xxx |
URL 丑陋、易泄露(贴链接就泄权)、不能被缓存 |
| 隐藏表单域 | <input type="hidden" name="sid"> |
只对表单提交有效 |
| 本地存储 + 请求头 | localStorage 存 token,JS 手动加 Authorization |
需要前端配合,且 localStorage 同样怕 XSS |
所以结论不是"必须用 Cookie",而是"必须有个地方存凭证并被自动/手动带上"。
9.3 CSRF:为什么 Cookie 会自动带,反而成了风险
Cookie 的核心特性是"同域名请求自动携带"。这在登录态上是优点,但也意味着:
你在
bank.com登录着,然后访问了恶意站点evil.com,它页面里藏了一个
<img src="http://bank.com/transfer?to=hacker&money=10000">。浏览器一加载这个图片,就自动带着你的 Cookie 向银行发了转账请求 ------ 你什么都没点,钱没了。
这就是 CSRF(跨站请求伪造)。防御手段:
| 手段 | 原理 |
|---|---|
SameSite=Lax/Strict |
跨站请求不带 Cookie(Lax 允许顶级导航的 GET),一行属性挡掉大部分攻击 |
| CSRF Token | 表单里带一个服务端生成、攻击者猜不到的随机串,服务端校验 |
校验 Referer / Origin |
请求从哪来的 |
| 敏感操作用 POST + 二次确认 | 不能让 GET 产生副作用 |
注意区分 XSS 和 CSRF :XSS 是"在你的站点里执行攻击者的 JS "(偷数据/偷 Cookie),CSRF 是"借你的身份在别的站点发请求 "(借权限)。
HttpOnly防 XSS 偷 Cookie,SameSite防 CSRF。
9.4 分布式 Session 的三种解法
单机 unordered_map 一上多台服务器就废了:用户第一次请求落到 A,第二次落到 B,B 根本不认识这个 ID。三种方案:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 粘性会话(Sticky Session) | 负载均衡按 IP / Cookie 把同一用户固定打到同一台 | 零改造 | 一台挂了用户就掉线,扩容时分布不均 |
| 集中存储(主流) | Session 全放 Redis,所有节点共享 | 任意节点可服务、天然支持重启/扩容 | 多一次网络往返,Redis 要高可用 |
| 客户端 Token(JWT) | 状态签进 token 本身,服务端不存 | 完全无状态、易水平扩展 | 无法主动失效、token 泄露在有效期内都有效、体积大 |
面试常问:"JWT 和 Session 怎么选?"
需要强制下线 / 改密码即踢人 / 精细化权限 → Session;
需要跨服务、无状态、高并发 → JWT(并配合短过期 + refresh token)。
9.5 一张图看懂登录全流程
① POST /login (username, password)
─────────────────────────────▶
│ 校验密码
│ 生成 sid(128bit 随机)
│ sessions[sid] = {uid, 权限, 过期时间}
◀─────────────────────────────
200 OK + Set-Cookie: sid=xxx; HttpOnly; Secure; SameSite=Lax; path=/
② GET /index.html
Cookie: sid=xxx ────────────▶
│ 查 sessions[sid] → 命中 → 放行
◀─────────────────────────────
200 OK
③ POST /logout
Cookie: sid=xxx ────────────▶
│ erase(sessions, sid) ← 主动失效
◀─────────────────────────────
200 OK + Set-Cookie: sid=deleted; Max-Age=0
9.6 高频面试题速答
Q1:HTTP 是无状态的,那登录态是怎么维持的?
靠 Cookie + Session(或 Token)。协议本身仍无状态,状态是应用层额外维护的:浏览器每次带 Cookie,服务端用它查到会话数据。
Q2:Cookie 和 Session 的区别?
看 9.1 的表。核心:存储位置不同 (客户端 vs 服务端)、安全性不同 (Session 数据不落到客户端)、依赖性不同(Session 通常靠 Cookie 传 ID)。
Q3:Cookie 有哪些安全属性?
HttpOnly(防 JS 读,抗 XSS)、Secure(仅 HTTPS)、SameSite(抗 CSRF)、domain/path(缩小作用域)、加 __Host- / __Secure- 前缀可强制约束。
Q4:为什么登录成功后要重新生成 Session ID?
防 Session 固定攻击:攻击者先让你用一个他知道的 ID,你一登录,这个 ID 就变成有效凭证,他直接接管。
Q5:Session 存在哪?多了怎么办?
内存 → Redis → DB。量大就用 Redis,配 TTL 自动过期;节点多就用集中存储避免粘性会话。
Q6:禁用 Cookie 后 Session 怎么用?
URL 重写 / 隐藏表单域 / 前端存 token 走请求头。本质是把"传 ID"这件事换个通道。
Q7:为什么说 Cookie 天生不安全?
它在客户端:用户能改(篡改)、XSS 能读(窃取)、明文 HTTP 能截(中间人)。所以敏感数据不能放 Cookie,只放不可猜的 ID。
十、总结
| 机制 | 存哪 | 解决什么 | 一句话 |
|---|---|---|---|
| Cookie | 浏览器 | 让请求"自带身份" | 服务器塞给浏览器的一小块数据,之后每次都自动带回 |
| Session | 服务器 | 让服务端"记住你是谁" | 一份服务端档案,靠 Cookie 里的 ID 索引 |
课件的总结说得很准:
HTTP Cookie 和 Session 都是用于在 Web 应用中跟踪用户状态 的机制。Cookie 存储在客户端,Session 存储在服务器端,各有优缺点,实际应用中通常结合使用,以达到最佳的用户体验和安全性。
再补一句工程视角的话:
它们都不是 HTTP 协议的一部分 ,而是构建在 HTTP 之上的应用层约定。理解这一点,你就明白为什么"HTTP 无状态"和"网站记得我"可以同时成立。
附录:favicon.ico 是什么
favicon.ico是网站图标,显示在浏览器标签页、地址栏旁、收藏夹里。favicon= "favorite icon" 的缩写,.ico是图标文件格式。- 浏览器在请求页面时,会专门额外构造一个 HTTP 请求 去拿它(默认请求
/favicon.ico)。 - 这个请求同样会携带 Cookie,所以服务端日志里会看到两条"活跃"记录 ------ 不用管它,也可以自己放一个图标文件省得 404。