首页 / 知识库 / python服务端进阶 / 解析用户登录场景案例

03 - 路线 A:Session + Cookie(服务端记账本)

会话保持的第一条路线,也是最经典、最好理解的一条:Session + Cookie

它的核心思想一句话就能说完:

服务端自己记账本,发给你一个无意义的”手环号”,账本上记着”这个手环号 = 张三”。

一个精确的类比:健身房手环

回想第 01 章那个健忘症老板。这次他学聪明了,用了手环制度:

你办完卡(认证通过):
   前台给你一个手环,上面只印着一个号码:3 号
   同时,前台在自己的登记本上写:3 号 = 张三,VIP,今天有效

                          这个登记本就是 Session(服务端的账本)

你之后进任何区域:
   你:(亮出手环)3 号
   工作人员:(翻登记本)3 号……是张三,VIP,放行 ✅

关键点在于:

  • 手环上只有号码,没有任何个人信息。捡到手环的人光看号码,不知道你是谁、是不是 VIP。
  • 真正的信息(你是谁)记在前台的登记本里,也就是服务端。
  • 工作人员每次都要翻登记本才知道你是谁。
类比技术名词是什么存在哪
登记本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 目录