扫码登录

目录

后端处理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 自动删除 uuidWeb 端轮询到 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"}

image.png

状态管理

 状态管理
		pending等待扫码
		scanned已扫描等待确认
		authorized已授权返回 token
		canceled用户取消
		expired二维码过期

 APP 端和 Web 端共识
		Web 端轮询 /login/status/{uuid}查询状态
		微信小程序调用 /wechat/scan/{uuid}标记扫码状态
		用户点击确认登录APP 端调用 /wechat/confirm/{uuid}
		用户点击取消登录APP 端调用 /wechat/cancel/{uuid}

 二维码信息存储
		二维码直接编码 uuidWeb  & APP 端可共享 uuid

 自动清理
		uuid 40 秒后失效防止无限轮询
		login 状态 5 分钟超时防止 Redis 占用

难点处理

难点场景处理1:

  1. 用户在 二维码有效期内(40 秒)扫码 了,Web 端的轮询 /login/status/{uuid} 状态变为 “scanned”

  2. 用户没有立即点击同意授权,而是 拖了一会儿(可能 1-2 分钟后才点授权)

  3. 但是 二维码有效期 40 秒已过,Redis 已经自动删除 uuid。

  4. 用户点击授权时,后端 /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 可能被前端劫持,安全性降低

点赞一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦