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

06 - 完整例子:一次”登录 → 访问 → 退出”的旅程

前面五章的概念都是零散的零件。这一章我们用一个真实的用户操作序列,把它们全串起来,像看电影一样过一遍:张三打开 App,登录,看了看订单,然后退出。

只讲思路,配几行关键代码点睛。看完你脑子里应该能”放电影”一样过一遍整个流程。

本章用 JWT + Redis 黑名单 这套”混用”方案来演示,因为它把前面所有零件(认证、JWT、Redis、退出难题)都用上了,最有代表性。技术栈:FastAPI + Redis。

场景设定

张三在 App 里要做三件事,我们跟着这三件事,看服务端每一步经历了什么:

  1. 登录:输入账号密码,点”登录”
  2. 访问:进”我的订单”页
  3. 退出:点”退出登录”

第一幕:登录——验明正身,发一张通行证

张三输入 zhangsan / 123456,点登录。App 发出请求:

App ──POST /login  { username: zhangsan, password: 123456 }──▶ 服务端

服务端接到后,做的是第 02 章说的第一件事:认证

┌─────────────────────────────────────────────┐
│ 服务端 · 登录处理                              │
│                                               │
│ ① 从数据库捞出 zhangsan 这条记录               │
│ ② 把提交的密码 123456 做哈希,和库里存的哈希比对 │
│      (注意:库里存的是哈希,不是明文!第08章)  │
│ ③ 对得上 → 认证通过;对不上 → 返回 401,结束    │
│                                               │
│ ④ 认证通过,开始第二件事:会话保持              │
│    签发一张 JWT:{ sub: zhangsan, exp: 2h后 }  │
│    用密钥盖上防伪签名                           │
└─────────────────────────────────────────────┘

App ◀── 返回 { "token": "eyJhbGc..." } ── 服务端

        └─ App 把这个 token 存起来(比如手机本地存储)
@app.post("/login")
def login(form: LoginForm):
    user = db.get_user(form.username)
    if not user or not verify_password(form.password, user.hashed_password):
        raise HTTPException(401, "用户名或密码错误")     # 认证失败
    token = jwt.encode({"sub": user.id, "exp": now + timedelta(hours=2)}, SECRET_KEY)
    return {"token": token}                              # 发证

这一幕的关键:认证只在这里做一次。之后张三再也不用输密码了——通行证已经在他手上。

第二幕:访问——亮出通行证,服务端认人

张三点进”我的订单”。App 这次要主动把 token 带上(JWT 方案前端要手动带,回顾第 04 章):

App ──GET /my/orders   Header: Authorization: Bearer eyJhbGc...──▶ 服务端

服务端做的是第 02 章的第二件事:会话保持——靠通行证重新认出张三:

┌───────────────────────────────────────────────┐
│ 服务端 · 每个受保护接口都会先过这一关            │
│                                                 │
│ ① 从请求头拿出 token                            │
│ ② 用密钥验签名 → 内容有没有被篡改?             │
│ ③ 看 exp → 过期了没?                           │
│ ④ 【混用方案特有】去 Redis 查:这个 token       │
│    在黑名单里吗?(是不是已经退出登录了?)      │
│      └─ 在黑名单 → 拒绝;不在 → 继续             │
│ ⑤ 全部通过 → 从 token 内容读出"是张三"          │
└───────────────────────────────────────────────┘


   查数据库,返回张三的订单

App ◀── 张三的订单列表 ──
def get_current_user(token: str = Header(..., alias="Authorization")):
    raw = token.replace("Bearer ", "")
    try:
        payload = jwt.decode(raw, SECRET_KEY, algorithms=["HS256"])   # 验签 + 验过期
    except JWTError:
        raise HTTPException(401, "token 无效或已过期")
    if redis.exists(f"blacklist:{raw}"):                              # 查黑名单
        raise HTTPException(401, "已退出登录,请重新登录")
    return payload["sub"]

@app.get("/my/orders")
def my_orders(user_id = Depends(get_current_user)):    # 加个依赖,接口就受保护了
    return db.get_orders(user_id)

注意第 ④ 步:纯 JWT 是不查任何存储的。我们这里加了一步查 Redis 黑名单,正是为了解决第三幕的退出问题——这也说明”混用方案”牺牲了一点点无状态的纯粹性,换来了控制力。

张三在 App 里翻了好几页订单、点了商品详情、看了物流……每一个请求都重复第二幕这套流程:带 token → 验签 → 查黑名单 → 认出是张三。他自己完全无感,感觉就是”一直登着”。

第三幕:退出——让通行证作废

张三点了”退出登录”。这里就是 JWT 方案最微妙的地方了。

回想第 04 章的伏笔:JWT 是发到用户手上的通行证,服务端本身不记账本,所以没法直接把它”收回来”。张三手机里那个 token 字符串,在它自己 2 小时到期之前,本来是一直有效的。

那怎么办?——服务端记一个”作废名单”(黑名单)。退出时,把这个 token 丢进 Redis 黑名单,并让黑名单记录的过期时间和 token 剩余寿命一样:

App ──POST /logout   Header: Authorization: Bearer eyJhbGc...──▶ 服务端

                    ┌─────────────────────────────────────────────┘

        把这个 token 加入 Redis 黑名单:
        SET blacklist:eyJhbGc... = 1  ,过期时间 = token 还剩多久到期

App ◀── { "msg": "已退出" } ──
@app.post("/logout")
def logout(token: str = Header(..., alias="Authorization")):
    raw = token.replace("Bearer ", "")
    payload = jwt.decode(raw, SECRET_KEY, algorithms=["HS256"])
    ttl = payload["exp"] - now_timestamp()          # token 还剩多久到期
    redis.setex(f"blacklist:{raw}", ttl, 1)          # 拉黑,到期后自动清理
    return {"msg": "已退出"}

从此以后,张三手机里那个 token 虽然签名依然有效、也还没到期,但只要它一来,第二幕的第 ④ 步就会发现”它在黑名单里”,直接拒绝。通行证等于作废了。

一个巧妙的小细节:黑名单记录的过期时间设成”token 剩余寿命”。因为 token 一旦自然到期,第 ③ 步就会拦下它,黑名单也就没用了,可以自动删除,不会让 Redis 无限膨胀。

把整个旅程连起来看

张三打开 App

[第一幕 · 登录] 输密码
   │  服务端:验密码(认证) → 签发 JWT → 返回 token
   │  App 保存 token

[第二幕 · 访问] 进"我的订单"(以及之后每一次操作)
   │  App:请求头带上 token
   │  服务端:验签 → 验过期 → 查黑名单 → 读出"是张三" → 返回数据
   │  (重复 N 次,张三无感,感觉一直登着)

[第三幕 · 退出] 点"退出登录"
   │  服务端:把 token 加入 Redis 黑名单(作废)
   │  之后这个 token 再来,第二幕第④步就拦下

结束

对照一下这一个流程里用到的所有概念:

环节用到的概念对应章节
验密码,确认是本人认证(只做一次)02
存的是哈希不是明文密码安全08
签发/携带/验证通行证JWT、会话保持04
每次请求前统一认人依赖注入做”守门”04 / 本章
退出登录靠黑名单JWT 退出难题07
黑名单存哪、怎么自动清理Redis + 过期时间03 / 07

小结

  • 一次完整登录旅程 = 登录(认证 + 发证)→ 访问(每次带证、验证、认人)→ 退出(作废证)
  • 认证只做一次,之后全靠通行证;用户”一直登着”的体感,来自第二幕在每个请求里默默认人。
  • JWT 退出登录靠 Redis 黑名单:把 token 拉黑,之后验证时多查一步就能拦住。
  • 黑名单过期时间设成 token 剩余寿命,既能作废、又能自动清理
  • 如果这一章的”电影”你能在脑子里顺下来,登录的核心思维你就建立起来了。

第三幕点到的”退出登录为什么这么绕”,值得单独深挖。下一章我们就专门讲:退出登录,看似简单,其实是整个登录体系里最难的一环。


上一章 ← 05 - 两条路线怎么选 | 下一章 → 07 - 退出登录:看似简单,其实最难 | 回到 README 目录