Cookie 与会话
Set-Cookie、HttpOnly、SameSite。
Cookie 与会话
这一课你将能做到
- 画出
Set-Cookie→ 浏览器存储 → 后续请求自动带Cookie头的完整往返,并说明它如何补上 HTTP 无状态的缺口 - 解释 HttpOnly、Secure、SameSite、Max-Age/Expires、Domain/Path 各防什么、各不防什么,并判断一条 Set-Cookie 写得安不安全
- 用 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();
}
}