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 记不住你

先看一个所有人都经历过的场景:

复制代码
未登录打开 B 站  →  点"登录"  →  扫码  →  登录成功
之后刷新任何页面  →  右上角还是我的头像
第二天再打开      →  仍然是登录态

课件提了两个问题:

  1. B 站是怎么认识我这个登录用户的?
  2. 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 存在客户端,存在被篡改或窃取的风险(用户自己也能改)。
  • 用途 (按重要性排序):
    1. 用户认证和会话管理(最重要)
    2. 跟踪用户行为(广告、统计)
    3. 缓存用户偏好(主题、语言)

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 也被广泛接受。记住结论就够了,不用背历史。

限制 数值 说明
单个 Cookie 大小 约 4KB 超了浏览器直接丢弃,所以不能塞大对象
单域名 Cookie 数量 约 50 个(因浏览器而异) 超了会按策略淘汰旧的
域名隔离 严格 a.com 的 Cookie 不会发给 b.com(同源策略的一部分)
端口不隔离 ------ 同一主机不同端口的 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。

所以正确的姿势是: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 用途

  1. 用户认证和会话管理
  2. 存储用户的临时数据(购物车是经典例子)
  3. 实现分布式系统的会话共享(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。

在 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(或两个不同浏览器 / 一个浏览器正常窗口 + 一个无痕窗口)。

  1. 清掉两个浏览器里该站点的所有 Cookie

    • 历史没做过测试可以跳过;
    • Chrome 尤其要清 :它可能缓存了历史 sessionid,导致你一访问就带着旧 ID,看不出"新建会话"的效果。打印一下请求头就能明白为什么要删。
  2. 两个浏览器分别访问 /login

    • 各自拿到一个不同的 sessionid;
    • 服务端日志出现两条 user-0 被添加 / user-1 被添加。
  3. 两个浏览器再访问任意站点资源

    • 浏览器自动带各自的 sessionid;
    • 服务端日志分别打印 user-0 正在活跃 / user-1 正在活跃。

结论:服务器已经能识别"这是哪个浏览器"了。 这就是登录态的最小闭环。

小技巧:观察日志时会看到 favicon.ico 的请求也带着 Cookie,也是"活跃"的------不用管它(见附录)。


九、必须掌握的扩展

维度 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",而是"必须有个地方存凭证并被自动/手动带上"。

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。
相关推荐
步行cgn1 小时前
HTTP 常用请求方法详解
网络·网络协议·http
mengge.cloud2 小时前
1019Namespace ,Contexts ,Kubernetes Pod
运维·服务器·网络·云原生·容器·kubernetes·云计算
艾iYYY2 小时前
【无标题】
linux·开发语言·网络·操作系统
QZ166560951592 小时前
可视化易用·低误报率·跨区域部署:知源-AI数据分类分级系统在金融行业的落地实践
大数据·网络·安全·网络安全
aixingkong9212 小时前
UALink 技术白皮书解读
服务器·网络·硬件架构
Amumu121383 小时前
Node学习: 网络基础概念 + HTTP模块
网络·node.js
tdtengrun3 小时前
“绿色轮胎安全周”走进校园,让安全与环保理念扎根童心
网络·媒体
注塑机数据采集联网4 小时前
注塑机数据采集网关读取注塑机工艺/SPC数据计算原理
大数据·网络·数据仓库·物联网·智能路由器·制造
mmsx4 小时前
Android APK 加固方案怎么落地:从加壳到校验的组合取舍
android·网络·安全·签名