CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
HTTP 协议与缓存/课时/Cookie 与会话
RawBlame
课时目录12 节

HTTP 协议与缓存

  1. 01HTTP 是什么
  2. 02方法与语义
  3. 03状态码
  4. 04请求与响应头
  5. 05HTTPS 与 TLS
  6. 06HTTP 缓存
  7. 07Cookie 与会话
  8. 08CORS
  9. 09REST 与资源设计
  10. 10HTTP/2 与 HTTP/3
  11. 11应用层缓存对照
  12. 12综合练习

Cookie 与会话

Set-Cookie、HttpOnly、SameSite。

25 分钟

Cookie 与会话

这一课你将能做到

  1. 画出 Set-Cookie → 浏览器存储 → 后续请求自动带 Cookie 头的完整往返,并说明它如何补上 HTTP 无状态的缺口
  2. 解释 HttpOnly、Secure、SameSite、Max-Age/Expires、Domain/Path 各防什么、各不防什么,并判断一条 Set-Cookie 写得安不安全
  3. 用 DevTools Application → Cookies 面板与 curl -c/-b 模拟「登录后带票访问」,说清 Cookie 会话与 Bearer Token 的取舍

为什么要学

协议不记得你是谁,网站却要登录态——Cookie(或前端自己存的 Token)就是补上这个缺口的胶水。属性配错全是真实事故模式:不设 HttpOnly 被 XSS 脚本偷走会话票、不设 Secure 在 HTTP 环境泄露、SameSite 配 None 又不防 CSRF 导致用户被伪造请求。

面试必问「Cookie 和 Session 的区别」「Session 是怎么实现的」「CSRF 怎么防」,本站管理端就是 Sa-Token + Redis 会话的活样板。这一课学完,登录相关的八成线上问题你都能定位。

先建立图像

登录往返全景:

① 登录请求
POST /api/auth/login HTTP/1.1
Content-Type: application/json

{"username":"admin","password":"***"}

② 服务器发票
HTTP/1.1 200 OK
Set-Cookie: satoken=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=7200

③ 之后每次请求,浏览器自动带上
GET /admin/tracks HTTP/1.1
Cookie: satoken=abc123
浏览器侧:按域名隔离的 cookie jar
  └─ name=value + 一堆属性(Path/Domain/HttpOnly/…)

服务器侧:通常只认票号
  Cookie 里的 satoken ──查──► Redis 里的会话详情(多实例共享)
属性人话防什么 / 不防什么
HttpOnly页面 JS 读不到防 XSS 偷票;不防「恶意脚本以你的身份发请求」
Secure仅 HTTPS 连接才发送防明文链路泄露;本地 http 调试会「失效」
SameSite=Lax/Strict/None跨站请求带不带这张票Lax/Strict 减 CSRF;None 必须 Secure
Domain / Path哪些域名/路径会带上收窄=攻击面小;设父域=子域共享风险
Max-Age / Expires有效期都不设 = 会话级,关浏览器即丢

跟着做

步骤 1:在 Spring Boot 里亲手发一张票

写一个最小登录演示(真实项目用 Sa-Token/Spring Security,这里看协议本质):

@RestController
@RequestMapping("/api/auth")
public class AuthDemoController {

    private final Map<String, String> sessions = new ConcurrentHashMap<>(); // 生产换 Redis

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody Map<String, String> body) {
        if (!"admin".equals(body.get("username")) || !"123456".equals(body.get("password"))) {
            return ResponseEntity.status(401).body(Map.of("error", "用户名或密码错误"));
        }
        String token = UUID.randomUUID().toString().replace("-", "");
        sessions.put(token, "admin");                       // 服务端记会话
        ResponseCookie cookie = ResponseCookie.from("DEMO_TOKEN", token)
                .httpOnly(true)                             // JS 读不到
                .secure(false)                              // 本地 http 调试先关,上生产必须 true
                .sameSite("Lax")                            // 防 CSRF 第一道
                .path("/")
                .maxAge(Duration.ofHours(2))
                .build();
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, cookie.toString())
                .body(Map.of("msg", "login ok"));
    }

    @GetMapping("/me")
    public Map<String, String> me(@CookieValue(value = "DEMO_TOKEN", required = false) String token) {
        if (token == null || !sessions.containsKey(token)) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "未登录");
        }
        return Map.of("user", sessions.get(token));
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(@CookieValue(value = "DEMO_TOKEN", required = false) String token) {
        if (token != null) sessions.remove(token);          // 服务端使票作废
        ResponseCookie cleared = ResponseCookie.from("DEMO_TOKEN", "").maxAge(0).path("/").build();
        return ResponseEntity.noContent().header(HttpHeaders.SET_COOKIE, cleared.toString()).build();
    }
}

步骤 2:curl -c / -b 模拟浏览器带票

# -c:把 Set-Cookie 存进 cookie jar
curl -s -c jar.txt -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" -d '{"username":"admin","password":"123456"}'

# 打开 jar.txt 看看浏览器替你保管了什么
cat jar.txt

# -b:下次请求带上这张票 → 200
curl -sb jar.txt -i http://localhost:8080/api/auth/me

# 不带票 → 401
curl -i http://localhost:8080/api/auth/me

# 注销后旧票立即失效(服务端 sessions 已删)
curl -sb jar.txt -X POST http://localhost:8080/api/auth/logout -o /dev/null -w "%{http_code}\n"
curl -sb jar.txt -i http://localhost:8080/api/auth/me    # 401

这一组命令完整演示了「发票 → 带票 → 验票 → 废票」四步。

步骤 3:DevTools 看票的属性

  1. 浏览器访问登录页并登录(可用任意已登录站点)
  2. F12 → Application → Cookies → 选当前域名
  3. 逐列检查:HttpOnly 是否勾了、SameSite 是什么、Expires/Max-Age 到期时间

再切到 Network:任选一个同站请求,Request Headers 里的 cookie: 就是自动带上的——你没写任何代码,浏览器替你干了。

步骤 4:HttpOnly 的「看不见」实验

登录后打开 Console 执行:

document.cookie

HttpOnly 的会话票不会出现——这是好事:就算页面被注入恶意脚本,也读不走票。注意区分:读不到 ≠ 防不住,脚本仍可以借你的浏览器发请求(XSS 的破坏面在下面 Q&A)。

步骤 5:多实例下会话必须放 Redis

单机 Map 挂两个实例就会「A 实例登录、B 实例不认」。生产做法:会话详情存 Redis,Cookie 里只带票号:

Cookie: satoken=abc123
            │
            ▼
Redis:  key = satoken:abc123
        value = {userId, roles, expireAt...}   ← TTL 就是会话有效期

本站管理端即 Sa-Token + Redis 集成。失效有两把刀:浏览器清 Cookie(用户侧),Redis 过期或主动 DEL(服务端踢下线)。这也解释了为什么「退出登录」必须调服务端接口——只清前端存储,Redis 里的票还活着。

步骤 6:SameSite 三值情景题

  • 从微信里点链接跳进本站(顶级 GET 导航):Lax 会带票,登录态正常
  • 外站页面里藏一个自动提交的表单 POST 到本站:Lax 不带票 → CSRF 被挡
  • 本站要被别的站以 iframe/跨站 fetch 嵌入并带票:只能 SameSite=None; Secure,同时必须补 CSRF Token 等防线

讲清楚要点

  1. 因为 HTTP 无状态,所以「登录了」必须在每个后续请求里可验证——Cookie 或 Authorization 头是载体,不存在「服务器自动记住」的魔法。
  2. 因为 Cookie 由浏览器自动附带,所以 CSRF 攻击主要盯 Cookie 方案——SameSite=Lax 是低成本第一道防线,关键写操作再加 CSRF Token 或校验 Origin。
  3. 因为 HttpOnly 挡的是「脚本读票」,挡不住「已注入的脚本替用户发请求」——防 XSS 还是要靠输出转义、CSP;HttpOnly 只是让偷票变难。
  4. 因为 Secure 要求 HTTPS,所以本地 HTTP 调试时「登录成功但 Cookie 存不上」不是 Bug——开发环境关 Secure 或本地配自签 HTTPS,但生产配置必须 Secure。
  5. 因为 Domain 写成父域会让所有子域共享票,所以能收窄就收窄——任何一个子域被打穿(如测试环境 XSS),主站会话跟着遭殃。
  6. 因为会话的权威在服务端(Redis),所以「退出登录」必须服务端作废票据——客户端清存储只是丢票,票号本身若没失效等于还被人拿着。

你可能会懵的地方

  1. Cookie 和 Session 是一回事吗? 不是。Cookie 是浏览器侧的存储与自动携带机制;Session 是服务端的会话状态。经典组合「Session + Cookie」= 服务端存状态、Cookie 只带 session id。JWT 方案则把状态签名后自包含在 Token 里,服务端可不存 Session——但吊销和提前过期变难。

  2. 为什么有了 SameSite=Lax 还要 CSRF Token? Lax 只拦跨站的「非安全方法」,同站内的攻击、老浏览器、以及 SameSite=None 的嵌入场景都还有暴露面。关键操作(改密码、支付)双保险:Token 校验或要求二次确认。

  3. 第三方 Cookie 要被全面禁用了,登录怎么办? 被收紧的是跨站跟踪类 Cookie。第一方登录 Cookie(用户直接访问你的站)不受影响。嵌入场景(第三方登录、iframe 支付)要用 CHIPS(分区 Cookie)、Storage Access API 等新方案迁移。

  4. curl 能通浏览器不通,是不是 Cookie 的问题? 高频原因之一:浏览器同源请求自动带 Cookie,fetch 默认 same-origin(跨源不带),curl 忘了 -b。逐项对照「谁没带票」:看 Network 请求头里有没有 cookie:,再看服务端日志里票校验失败的原因。

  5. Max-Age 和 Expires 哪个优先? 同时存在时 Max-Age 优先(Expires 是老规格、秒级精度差、还受客户端时钟影响)。都缺省就是会话级 Cookie,关闭浏览器即失效——但现代浏览器有会话恢复机制,别把「关浏览器就登出」当成安全保证。

错法对照

错法问题正解
会话 Cookie 不设 HttpOnly一处 XSS 就能偷走所有用户会话会话票默认 HttpOnly
生产 Cookie 无 Secure 标志用户在 HTTP 环境(或降级攻击下)明文带票全站 HTTPS + Secure
SameSite=None 却忘了 Secure规范要求 None 必须 Secure,浏览器直接拒收该 Cookie成对配置,或改回 Lax
退出登录只清 localStorage服务端票仍有效,等于没退调服务端登出接口作废会话 + 清 Cookie
把长期 refresh token 存在可被 JS 读的 localStorage 且无轮换XSS 偷走长效凭证HttpOnly Cookie 承载 + 短期 access token + 轮换/吊销机制

卡点急救

  1. 「登录成功但刷新又变未登录」:先看 Set-Cookie 响应头是否真的下发(Network → 登录请求 → Response Headers);再看 Application → Cookies 里有没有存上——常见原因是 Secure 在 HTTP 本地环境不生效、Domain 与当前域名不匹配、SameSite=None 缺 Secure 被浏览器丢弃。
  2. 接口一会儿 200 一会儿 401:多实例 + 会话存内存的典型症状(负载均衡轮到没存你票的实例)。会话迁 Redis,或确保负载均衡 sticky(治标);检查 Redis TTL 是否被配置得过短。
  3. 跨域前端拿不到/带不上 Cookie:三件套齐全才行——前端 credentials: 'include';后端 CORS 回具体 Origin(不能 *)且 Access-Control-Allow-Credentials: true;Cookie 的 SameSite/Domain 允许该场景。缺一不可(CORS 课细讲)。

面试常问

  • Cookie 和 Session 的区别?分布式环境下 Session 怎么办? Cookie 存在客户端、随请求自动携带、有大小(约 4KB)与数量限制;Session 是服务端会话状态,Cookie 里通常只存 session id。分布式下单机内存 Session 会被负载均衡打散,方案:Session 集中到 Redis(Spring Session / Sa-Token+Redis,主流)、客户端自包含 JWT、或粘性会话(不推荐为长期方案)。

  • CSRF 是什么?Cookie 方案怎么防? 跨站请求伪造:用户在 A 站已登录,访问恶意 B 站时,B 偷偷向 A 发请求——浏览器会自动带上 A 的 Cookie,请求「看起来」是本人发的。防护:SameSite=Lax/Strict 让跨站请求不带票、CSRF Token(服务端下发、提交时校验)、校验 Origin/Referer。JWT 放 Authorization 头天然不自动携带,CSRF 面更小,但要防 XSS。

  • HttpOnly、Secure、SameSite 分别解决什么问题? HttpOnly:JS 读不到 Cookie,缓解 XSS 偷票。Secure:只在 HTTPS 上发送,防明文泄露。SameSite:控制跨站请求是否携带,缓解 CSRF。三者都只是降低攻击面,不能替代 XSS/CSRF 的完整防护。

自测三问

  1. 从登录成功到第二次带态请求,Cookie 在协议上经历了哪两步?分别由谁执行?
  2. HttpOnly、Secure、SameSite 各解决哪类风险?各举一个「它解决不了」的例子。
  3. Cookie 会话与 Bearer Token 在「浏览器是否自动携带」和「CSRF 暴露面」上的核心差别是什么?

小结一句话

Cookie 是无状态 HTTP 上的回执号:HttpOnly/Secure/SameSite 决定它能不能被偷、被跨站滥用;会话详情常落 Redis,退出必须服务端作废。

延伸

  • MDN:HTTP Cookie
  • MDN:Set-Cookie
  • 下一课:CORS——跨源请求被浏览器拦截时的红字,为什么 curl 通浏览器不通:同源策略、预检 OPTIONS 与 Spring 的 CORS 配置。
加载本节练习…
← 上一课HTTP 缓存下一课 →CORS