第2章 支付SDK中OAuth2.0授权码模式的代码框架搭建:Flask/Django路由、回调处理、令牌存储
授权码模式(Authorization Code)是OAuth2.0协议中最安全、最适用于服务端支付的流程。本章将手把手在Flask与Django中搭建核心代码框架,涵盖路由注册、回调端点实现和令牌存储策略。
2.1 流程原理与关键组件
授权码模式分为四个角色:资源拥有者(用户)、客户端(支付SDK后端)、授权服务器(支付平台OAuth服务)和资源服务器(支付API)。完整交互如下:
- 用户点击“授权支付” → 客户端构造授权链接,重定向至授权服务器。
- 用户确认授权 → 授权服务器返回授权码(code)到客户端回调地址。
- 客户端使用授权码 + client_secret 向令牌端点换取 access_ 和 refresh_。
- 客户端使用 access_ 调用支付API。
关键点:授权码仅一次性有效,且必须通过服务端交换令牌,避免令牌暴露在浏览器端。
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 调试与常见错误
- redirect_uri mismatch:授权服务器要求回调URL必须与注册时完全一致(包括协议、端口)。
- state验证失败:session丢失或跨站请求导致state不匹配。确认启用了session中间件,且使用了随机生成的state。
- 授权码已过期:授权码有效期极短(通常60秒),必须在回调中立即交换令牌。
- 令牌交换返回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. 令牌存储使用持久化数据库+加密字段,并配置自动清除过期令牌。