03 - 路线 A:Session + Cookie(服务端记账本)
会话保持的第一条路线,也是最经典、最好理解的一条:Session + Cookie。
它的核心思想一句话就能说完:
服务端自己记账本,发给你一个无意义的”手环号”,账本上记着”这个手环号 = 张三”。
一个精确的类比:健身房手环
回想第 01 章那个健忘症老板。这次他学聪明了,用了手环制度:
你办完卡(认证通过):
前台给你一个手环,上面只印着一个号码:3 号
同时,前台在自己的登记本上写:3 号 = 张三,VIP,今天有效
▲
这个登记本就是 Session(服务端的账本)
你之后进任何区域:
你:(亮出手环)3 号
工作人员:(翻登记本)3 号……是张三,VIP,放行 ✅
关键点在于:
- 手环上只有号码,没有任何个人信息。捡到手环的人光看号码,不知道你是谁、是不是 VIP。
- 真正的信息(你是谁)记在前台的登记本里,也就是服务端。
- 工作人员每次都要翻登记本才知道你是谁。
对应到技术上:Session 和 Cookie 分别是什么
| 类比 | 技术名词 | 是什么 | 存在哪 |
|---|---|---|---|
| 登记本 | Session | 记着”某个号码对应的用户信息” | 服务端(内存 / Redis / 数据库) |
| 手环上的号码 | Session ID | 一串随机、无意义的字符串 | 服务端生成,发给客户端 |
| 一直戴着的手环 | Cookie | 浏览器自动保存、每次请求自动带上的小纸条 | 客户端(浏览器) |
一句话串起来:服务端把用户信息存在 Session 里,把对应的 Session ID 塞进 Cookie 发给浏览器;浏览器之后每次请求自动带上这个 Cookie,服务端拿 Session ID 去查 Session,就知道你是谁。
完整流程图
① 登录
浏览器 ──POST /login (张三/密码)──▶ 服务端
│ 认证通过
│ 生成随机 Session ID:"abc123"
│ 在 Redis 里记:abc123 → {user: 张三, role: VIP}
│
浏览器 ◀── 响应头 Set-Cookie: session_id=abc123 ── 服务端
│
└─ 浏览器自动把 session_id=abc123 存进 Cookie
② 之后每次请求(浏览器自动带 Cookie,不用前端写代码)
浏览器 ──GET /my/orders Cookie: session_id=abc123──▶ 服务端
│ 拿 abc123 去 Redis 查
│ 查到:是张三!
│ 返回张三的订单
浏览器 ◀────────────── 张三的订单 ──────────────────
注意两个”自动”,这是 Cookie 方案最舒服的地方:
Set-Cookie后浏览器自动保存,前端不用手动存。- 之后每次请求浏览器自动带上 Cookie,前端不用手动加。
Session 存在哪?这决定了系统能不能扩展
Session 是服务端的账本,那这个账本放哪很关键。有三种典型选择:
方案 1:存进程内存(最简单,但有大坑)
┌──────────────┐
│ 服务器 A │ 内存里:abc123 → 张三
└──────────────┘
问题:只有 A 认得张三。一旦你部署了多台服务器……
方案 2:存 Redis(推荐 ✅)
┌────────┐ ┌────────┐ ┌────────┐
│服务器 A │ │服务器 B │ │服务器 C │
└───┬────┘ └───┬────┘ └───┬────┘
└───────────┼───────────┘
▼
┌─────────────┐
│ Redis │ abc123 → 张三
│(共享账本) │
└─────────────┘
谁都能查到,随便加机器都不怕
方案 3:存数据库(可以,但慢,一般不用于高频会话)
为什么内存方案是大坑? 假设你为了扛住流量部署了 3 台服务器,前面一个负载均衡随机分发请求:
登录请求 → 负载均衡分给了服务器 A → A 在自己内存记下 abc123=张三
下个请求 → 负载均衡分给了服务器 B → B 内存里根本没有 abc123 → "你谁啊?请重新登录"
用户会莫名其妙地一会儿登录一会儿掉线。这就是为什么生产环境几乎都把 Session 放进 Redis——让所有服务器共享同一个账本。这也正好呼应了你学过的 Redis:它快、支持过期时间,天生适合放 Session。
用几行代码点透(不用背,理解就行)
登录时,把用户信息写进 Redis,并设置过期时间(比如 30 分钟):
import uuid
@app.post("/login")
def login(form: LoginForm, response: Response):
user = authenticate(form.username, form.password) # 认证:验密码
if not user:
raise HTTPException(401, "用户名或密码错误")
session_id = uuid.uuid4().hex # 生成随机手环号
redis.setex(f"session:{session_id}", 1800, user.id) # 记账本,30分钟过期
response.set_cookie("session_id", session_id, httponly=True) # 发手环
return {"msg": "登录成功"}
之后的接口,靠一个依赖去”翻账本”认人:
def get_current_user(session_id: str = Cookie(None)):
if not session_id:
raise HTTPException(401, "未登录")
user_id = redis.get(f"session:{session_id}") # 翻账本
if not user_id:
raise HTTPException(401, "登录已过期") # 账本上没有 = 过期/退出了
return load_user(user_id)
@app.get("/my/orders")
def my_orders(user = Depends(get_current_user)): # 加一句依赖,就受保护了
return get_orders(user.id)
重点不是这几行代码,而是这个结构:登录时写账本(Redis),访问时翻账本认人。这就是 Session 方案的全部精髓。
这条路线的最大优点:退出登录特别简单
记住这一点,第 07 章会反复对比。因为状态在服务端,想让谁失效,把账本上那一页撕掉就行:
@app.post("/logout")
def logout(session_id: str = Cookie(None)):
redis.delete(f"session:{session_id}") # 从账本删掉,凭证立刻作废
return {"msg": "已退出"}
删掉之后,那个 Session ID 再来查账本就查不到了,等于立刻作废。服务端对凭证有绝对的控制权——这是 Session 方案相对 JWT 的核心优势。
小结
- Session 方案 = 服务端记账本 + 客户端拿一个无意义的手环号(Session ID)。
- Session 存在服务端(推荐 Redis),Cookie 是浏览器帮你自动携带 Session ID 的载体。
- 浏览器”自动存 Cookie、自动带 Cookie”,前端很省心。
- Session 存内存会导致多机器下登录态错乱,生产环境放 Redis 让多台服务器共享。
- 最大优点:服务端完全掌控凭证,退出登录只需删掉账本一页,极其干净。
- 代价:服务端要为每个在线用户维护状态(占内存),且强依赖 Cookie(跨域、非浏览器客户端会麻烦)。
下一章是另一条路线:Token / JWT。它的思路正好相反——服务端不记账本,把”你是谁”直接写进凭证里。这带来了极好的扩展性,但也埋下了”退出登录很难”的伏笔。
上一章 ← 02 - 登录到底在做什么 | 下一章 → 04 - 路线 B:Token + JWT | 回到 README 目录