后端处理qrcode(安全为主)
1. 前端请求 /login/qrcode/ 获取 uuid 和 qrcode。
2. 前端每秒轮询 /login/status/{uuid},后端分情况处理:
• 二维码过期(uuid 失效)→ 让前端请求新的二维码。
• 用户未扫码 → pending,前端继续轮询。
• 用户扫码 & 登录成功 → 返回 token,前端完成登录。
3. 用户扫码后,后端更新 Redis 状态,前端最终轮询到 token 后跳转。
登录状态流转:
二维码的 状态变更 由 APP 端(微信小程序) 负责:
1. 未扫描(初始状态)
• Web 端请求 /login/qrcode/ 生成二维码,二维码 uuid 存入 Redis 状态 pending。
• Web 端轮询 /login/status/{uuid},检查状态是否变化。
2. 已扫描,等待用户确认
• 微信小程序扫描二维码后,调用 /wechat/scan/{uuid},后端更新 Redis scanned 状态。
• Web 端轮询时返回 status: scanned,前端显示 “请在手机端确认登录”。
3. 已扫描,用户同意授权
• 用户点击 “确认登录”,微信小程序调用 /wechat/confirm/{uuid},后端绑定 openid,生成 token 并更新状态 authorized。
• Web 端轮询时返回 status: authorized,前端跳转登录。
4. 已扫描,用户取消授权
• 用户点击 “取消登录”,微信小程序调用 /wechat/cancel/{uuid},后端更新 Redis canceled。
• Web 端轮询到 status: canceled,提示 “用户取消登录”。
5. 二维码过期
• 40 秒内未扫码,Redis 自动删除 uuid,Web 端轮询到 expired,自动刷新二维码。
from fastapi import FastAPI, HTTPException
import redis
import uuid
import qrcode
import base64
from io import BytesIO
app = FastAPI()
# 连接 Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# 配置时间
QR_CODE_EXPIRE = 40 # 二维码有效时间 40 秒
LOGIN_EXPIRE = 300 # 登录状态保存 5 分钟
@app.get("/login/qrcode/")
def generate_qrcode():
"""生成微信扫码登录二维码"""
login_uuid = str(uuid.uuid4())
redis_client.setex(f"wx_login:{login_uuid}", LOGIN_EXPIRE, "pending") # 初始状态 pending
redis_client.setex(f"wx_qr_expire:{login_uuid}", QR_CODE_EXPIRE, "1")
# 生成二维码,二维码内容直接包含 UUID(小程序端解析出 UUID 后可直接调用 API)
qr_data = {"uuid": login_uuid, "created_at": int(time.time()), "expire_at": int(time.time()) + QR_CODE_EXPIRE}
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill="black", back_color="white")
buffered = BytesIO()
img.save(buffered, format="PNG")
img_base64 = base64.b64encode(buffered.getvalue()).decode()
return {"uuid": login_uuid, "qrcode": f"data:image/png;base64,{img_base64}", "expire": QR_CODE_EXPIRE}
#必须 1 秒轮询 /login/status/{uuid} 才能 快速响应用户扫码
@app.get("/login/status/{uuid}")
def check_login_status(uuid: str):
"""前端轮询检查扫码状态"""
key = f"wx_login:{uuid}"
qr_key = f"wx_qr_expire:{uuid}"
# 1️⃣ 检查二维码是否过期
if redis_client.get(qr_key) is None:
return {"status": "expired"}
# 2️⃣ 检查登录状态
status = redis_client.get(key)
if status is None:
return {"status": "expired"}
if status.startswith("token:"):
token = status.split(":")[1]
redis_client.delete(key) # 登录成功后删除
return {"status": "authorized", "token": token}
return {"status": status} # 可能返回 pending, scanned, canceled
@app.post("/wechat/scan/{uuid}")
def scan_qrcode(uuid: str):
"""微信小程序扫描二维码后调用,标记为已扫描"""
key = f"wx_login:{uuid}"
if redis_client.get(key) is None:
raise HTTPException(status_code=404, detail="二维码无效或已过期")
redis_client.setex(key, LOGIN_EXPIRE, "scanned") # 标记为已扫描
return {"status": "scanned"}
@app.post("/wechat/confirm/{uuid}")
def confirm_login(uuid: str):
"""用户确认授权后调用,绑定用户 Token"""
key = f"wx_login:{uuid}"
if redis_client.get(key) is None:
raise HTTPException(status_code=404, detail="二维码无效或已过期")
# 生成用户 Token(真实应用应使用 JWT)
user_token = f"fake_token_for_{uuid}"
redis_client.setex(key, LOGIN_EXPIRE, f"token:{user_token}") # 绑定 Token
return {"status": "authorized", "token": user_token}
@app.post("/wechat/cancel/{uuid}")
def cancel_login(uuid: str):
"""用户取消授权后调用"""
key = f"wx_login:{uuid}"
if redis_client.get(key) is None:
raise HTTPException(status_code=404, detail="二维码无效或已过期")
redis_client.setex(key, LOGIN_EXPIRE, "canceled") # 标记为取消
return {"status": "canceled"}
状态管理
✅ 状态管理
• pending(等待扫码)
• scanned(已扫描,等待确认)
• authorized(已授权,返回 token)
• canceled(用户取消)
• expired(二维码过期)
✅ APP 端和 Web 端共识
• Web 端轮询 /login/status/{uuid},查询状态。
• 微信小程序调用 /wechat/scan/{uuid},标记扫码状态。
• 用户点击确认登录,APP 端调用 /wechat/confirm/{uuid}。
• 用户点击取消登录,APP 端调用 /wechat/cancel/{uuid}。
✅ 二维码信息存储
• 二维码直接编码 uuid,Web 端 & APP 端可共享 uuid。
✅ 自动清理
• uuid 40 秒后失效,防止无限轮询。
• login 状态 5 分钟超时,防止 Redis 占用。
难点处理
难点场景处理1:
-
用户在 二维码有效期内(40 秒)扫码 了,Web 端的轮询 /login/status/{uuid} 状态变为 “scanned”。
-
用户没有立即点击同意授权,而是 拖了一会儿(可能 1-2 分钟后才点授权)。
-
但是 二维码有效期 40 秒已过,Redis 已经自动删除 uuid。
-
用户点击授权时,后端 /wechat/confirm/{uuid} 查不到 uuid,导致失败。
这个问题的本质是:
微信小程序扫码和用户点击授权是两个独立的操作,扫码成功不等于立即授权。
方案解决1:
• 扫码成功后,状态存活时间延长到 5 分钟(等待用户授权)
用户扫码成功,更新 uuid 存活时间
• 当 /wechat/scan/{uuid} 被调用(用户扫码时)
• 延长 wx_login:{uuid} 存活时间到 5 分钟,确保后续授权请求可用:
状态 说明 过期时间
pending 生成二维码,等待扫码 40 秒
scanned 用户扫码成功,延长状态有效期 5 分钟
authorized 用户授权,返回 token 5 分钟
canceled 用户取消授权 5 分钟
expired 二维码过期 40 秒
前端处理qrcode
下面展示了前端生成二维码逻辑
前端直接请求 https://open.weixin.qq.com/connect/qrconnect?… 生成二维码。
• 扫码后,微信回调前端页面(或 redirect_uri)。
• 前端拿到 code,再调用自己后端 /users/third_login/login_call/ 交换 token。
此时后端可以通过code 解析出对应的内容,这样也比较简单。
前端url
https://open.weixin.qq.com/connect/qrconnect?appid=w
&redirect_uri=https%3A%2F%2Fab.cn%2F
&response_type=code
&scope=snsapi_login
&state=wechat_redirect
https://ab.cn/?code=031SL0STZZH&state=wechat_redirect
如果你要做的是企业系统(后台管理、B2B、支付类应用):
✅ 建议用“后端 uuid 方式”
• 优点:安全,不暴露 code,可以掌控整个流程
• 前端 1 秒轮询 /login/status/{uuid},体验流畅
如果是社交网站、PC 端轻量级扫码登录:
✅ “前端直连微信”方式更简洁
• 优点:少了 Redis 轮询,体验更快
• 缺点:code 可能被前端劫持,安全性降低