flask支付回调必须校验签名,否则易被伪造;需用request.get_data()获取原始数据,微信回调处理xml并去bom,支付宝回调需排序过滤参数后验签;须幂等处理、记录原始数据、密钥环境严格匹配。

Flask 支付回调必须校验签名,否则直接拒收请求
微信和支付宝回调不是“收到就处理”,而是“收到+验签通过才处理”。不校验签名的接口等于把支付结果完全交给攻击者伪造——比如伪造一笔 0.01 元回调成 10000 元。Flask 默认不自动解析 application/xml 或原始 POST body,所以第一步不是写业务逻辑,而是正确拿到原始数据。
常见错误:用 request.form 或 request.get_json() 读微信回调(XML)或支付宝回调(application/x-www-form-urlencoded 但含签名字段),结果签名验证永远失败。
- 微信回调:Content-Type 是
application/xml,必须用request.get_data()读原始 bytes,再用官方 SDK 的wechatpayv3.verify_signature()或手动验签 - 支付宝回调:Content-Type 是
application/x-www-form-urlencoded,但签名字段sign和待验字段都在 form 中;需用request.form.to_dict()获取全部参数,剔除sign和sign_type后按规则拼接字符串,再用rsa.verify()验证 - Flask 路由必须声明
methods=['POST'],且不要加@app.route(..., methods=['GET', 'POST'])——支付平台只发 POST
微信回调里别用 flask.request.json,XML 解析要自己控制
微信支付回调是纯 XML,request.json 会返回 None,request.get_json() 会报错或解析失败。必须用原始 body,并注意编码:微信发的是 UTF-8 字节流,但可能带 BOM,需先 strip。
示例关键步骤:
from flask import request
@app.route('/wechat/notify', methods=['POST'])
def wechat_notify():
raw_body = request.get_data() # 必须用这个
if raw_body.startswith(b'\xef\xbb\xbf'):
raw_body = raw_body[3:] # 去 BOM
try:
xml_dict = parse_xml(raw_body.decode('utf-8')) # 自行实现或用 xmltodict
# 然后调用 verify_signature(..., message=raw_body, signature=xml_dict['sign'])
except Exception as e:
return 'FAIL', 400
- 不要在解析前做任何
.decode().strip(),BOM 和换行会影响签名原文 - 微信 v3 接口推荐用官方
wechatpayv3包,其verify_signature()内部依赖原始raw_body,传错就验不过 - 验签失败必须返回纯文本
'FAIL'(不能 JSON、不能 HTML),且 HTTP 状态码必须是 200 或 400 —— 微信只认响应体内容
支付宝回调验签前必须排序并过滤空值,顺序错一位就失败
支付宝要求将所有非空、非 sign、非 sign_type 的请求参数,按 key 字典序升序排列,拼成 k1=v1&k2=v2 字符串,再用 RSA 公钥验签。顺序错、空格多一个、value 多个 URL 编码都会导致验签失败。
- 用
request.form.to_dict()后,先del data['sign']和del data['sign_type'] - 过滤掉 value 为空字符串或
None的项:{k: v for k, v in data.items() if v not in ['', None]} - key 排序必须严格:
sorted(data.keys()),然后拼接:&'.join([f'{k}={data[k]}' for k in sorted_keys]) - 支付宝公钥格式必须是 PEM,且开头为
-----BEGIN PUBLIC KEY-----;如果用的是 PKCS#8 格式(以-----BEGIN RSA PUBLIC KEY-----开头),需先转换
回调处理中更新数据库必须用幂等设计,不能只靠订单号去重
微信/支付宝都可能重复推送回调(网络超时重试),只查一次 order_id 是否存在然后插入,会导致第二次回调时主键冲突或跳过更新。必须让同一笔支付通知无论来多少次,结果都一致。
- 推荐方案:用数据库唯一约束 +
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL),再用UPDATE ... WHERE order_id = ? AND status = 'pending'更新状态 - 更稳妥做法:回调中先查订单当前状态,若已是
success,直接返回成功响应,不执行任何业务逻辑 - 避免在回调里触发发短信、发邮件等副作用操作——这些应放到异步队列中,由单独 worker 消费已确认成功的事件
- 记录完整原始回调数据(
raw_body或request.form)到日志或表中,字段包括source(wx/alipay)、timestamp、out_trade_no、result_code,排查问题时比代码更有用
最易被忽略的一点:验签密钥和回调地址的环境必须严格匹配——测试用的微信商户 API 证书不能用于生产,支付宝沙箱公钥也不能用来验正式回调。密钥一旦混用,验签必然失败,而错误日志里往往只显示“验签失败”,不会告诉你用错了哪套密钥。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











