第2章 支付SDK中OAuth2.0授权码模式的代码框架搭建:Flask/Django路由、回调处理、令牌存储

授权码模式(Authorization Code)是OAuth2.0协议中最安全、最适用于服务端支付的流程。本章将手把手在Flask与Django中搭建核心代码框架,涵盖路由注册、回调端点实现和令牌存储策略。

2.1 流程原理与关键组件

授权码模式分为四个角色:资源拥有者(用户)、客户端(支付SDK后端)、授权服务器(支付平台OAuth服务)和资源服务器(支付API)。完整交互如下:

  1. 用户点击“授权支付” → 客户端构造授权链接,重定向至授权服务器。
  2. 用户确认授权 → 授权服务器返回授权码(code)到客户端回调地址。
  3. 客户端使用授权码 + client_secret 向令牌端点换取 access_ 和 refresh_。
  4. 客户端使用 access_ 调用支付API。
关键点:授权码仅一次性有效,且必须通过服务端交换令牌,避免令牌暴露在浏览器端。
授权码模式流程图 用户(浏览器) 客户端应用(Flask/Django) 授权服务器(支付平台) 回调端点 /callback 令牌存储(内存/数据库) 1. 发起授权 2. 重定向授权 3. 用户确认 4. 授权码 → 回调 5. 令牌交换 & 存储 说明:授权码仅用于交换令牌,不可暴露给前端;令牌存储需加密,生产环境推荐Redis或数据库。

2.2 Flask路由与回调处理实现

以下代码展示Flask应用中注册/authorize和/callback两个关键路由,使用requests库与授权服务器交互。

前置依赖:安装 flask 和 requests,并在环境变量中配置 CLIENT_ID、CLIENT_SECRET、REDIRECT_URI。
import os
import requests
from flask import Flask, request, redirect, session

app = Flask(__name__)
app.secret_key = os.urandom(24)

# 配置信息(应通过环境变量加载)
CLIENT_ID = os.environ.get("CLIENT_ID")
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
AUTH_URL = "https://pay.example.com/oauth/authorize"
_URL = "https://pay.example.com/oauth/"
REDIRECT_URI = "http://localhost:5000/callback"

@app.route("/authorize")
def authorize():
    # 构造授权链接(state用于CSRF防护)
    state = os.urandom(16).hex()
    session["oauth_state"] = state
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "state": state,
        "scope": "payment"
    }
    auth_link = requests.Request("GET", AUTH_URL, params=params).prepare().url
    return redirect(auth_link)

@app.route("/callback")
def callback():
    # 验证state防止CSRF攻击
    state_received = request.args.get("state")
    if state_received != session.pop("oauth_state", None):
        return "Invalid state parameter", 400

    code = request.args.get("code")
    if not code:
        return "Authorization code missing", 400

    # 用授权码交换访问令牌
    _data = {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    resp = requests.post(_URL, data=_data)
    if resp.status_code != 200:
        return " exchange failed", 400

    _json = resp.json()
    # 存储令牌(示例存入session,生产应使用数据库)
    session["access_"] = _json["access_"]
    session["refresh_"] = _json.get("refresh_")
    session["_expiry"] = _json.get("expires_in")

    return "Authorization successful!  stored in session."

2.3 Django路由与回调处理实现

Django实现类似,通过views.py和urls.py分离逻辑。以下为关键视图代码:

views.py

import os, secrets
import requests
from django.shortcuts import redirect, HttpResponse
from django.views.decorators.http import require_GET

CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
AUTH_URL = "https://pay.example.com/oauth/authorize"
_URL = "https://pay.example.com/oauth/"
REDIRECT_URI = "http://localhost:8000/oauth/callback"

@require_GET
def authorize(request):
    state = secrets._hex(16)
    request.session["oauth_state"] = state  # 需要启用session中间件
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "state": state,
        "scope": "payment",
    }
    auth_link = requests.Request("GET", AUTH_URL, params=params).prepare().url
    return redirect(auth_link)

@require_GET
def callback(request):
    state_received = request.GET.get("state")
    if state_received != request.session.pop("oauth_state", None):
        return HttpResponse("Invalid state", status=400)

    code = request.GET.get("code")
    if not code:
        return HttpResponse("Code missing", status=400)

    _data = {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    resp = requests.post(_URL, data=_data)
    if resp.status_code != 200:
        return HttpResponse(" exchange failed", status=400)

     = resp.json()
    # 建议存储到数据库模型
    request.session["access_"] = ["access_"]
    request.session["refresh_"] = .get("refresh_")
    request.session["_expiry"] = .get("expires_in")

    return HttpResponse("OAuth callback completed.  saved.")

urls.py 路由配置

from django.urls import path
from . import views

urlpatterns = [
    path('authorize/', views.authorize, name='oauth_authorize'),
    path('callback/', views.callback, name='oauth_callback'),
]

2.4 令牌存储策略

令牌存储需要平衡安全性、持久性和性能。下表对比常见方案:

存储方式安全性持久性性能适用场景
内存/Session中等(依赖Server端隔离)低(进程重启丢失)极高开发调试、单实例应用
文件(JSON/数据库)高(加密存储)高中生产单机部署
Redis(内存数据库)高(可设过期+加密)中(依赖RDB/AOF)极高高并发、分布式生产环境
数据库(MySQL/PostgreSQL)极高(行级加密+TLS)高中需要审计、多实例共享
安全警告:切勿将 access_、refresh_ 直接存入前端Cookie或LocalStorage。服务端存储时务必加密令牌字段,并设置合理的过期时间。

2.5 调试与常见错误

  1. redirect_uri mismatch:授权服务器要求回调URL必须与注册时完全一致(包括协议、端口)。
  2. state验证失败:session丢失或跨站请求导致state不匹配。确认启用了session中间件,且使用了随机生成的state。
  3. 授权码已过期:授权码有效期极短(通常60秒),必须在回调中立即交换令牌。
  4. 令牌交换返回400:检查client_secret是否正确,以及是否在授权服务器后台配置了允许的授权类型。

调试时可开启requests库的日志输出:

import logging
logging.basicConfig(level=logging.DEBUG)
requests_logger = logging.getLogger('urllib3')
requests_logger.setLevel(logging.DEBUG)

2.6 性能与稳定性优化

  • 令牌刷新自动化:在拦截器或装饰器中检测access_过期,自动使用refresh_获取新令牌。
  • 连接池与超时:使用requests.Session()复用连接,设置timeout避免接口阻塞。
  • 后端缓存令牌:对频繁使用的令牌添加本地缓存(如lru_cache),减少外部存储访问次数。
  • 异步回调处理:如果授权服务器返回频率高,可改用Celery或异步任务队列处理令牌交换,避免阻塞Web worker。
生产上线检查清单: 1. 将 CLIENT_SECRET 放入密钥管理服务(如Vault),禁止硬编码。 2. 回调端点添加速率限制(Rate Limiting),防止暴力重放。 3. 开启HTTPS,回调地址仅允许TLS。 4. 令牌存储使用持久化数据库+加密字段,并配置自动清除过期令牌。