资讯详情

Python接口数字签名实战:让Flask/Django API防篡改防重放

📅 2026/10/22 5:17:41 | 华诺云谱 👁 阅读
Python接口数字签名实战:让Flask/Django API防篡改防重放
现在不少做后端的朋友会跟我聊API 都上了 HTTPS是不是就够安全了其实有实战经验的人都会告诉你HTTPS 解决的是传输层加密防止数据在网络上裸奔。但到了应用层请求从客户端发出来到服务端接收到中间经过网关、日志、代理、甚至是接入层框架的格式化处理数据到底有没有被动过手脚HTTPS 是管不到的。这就是为什么很多开放平台、支付系统、对外服务的 API 都会在业务参数之外再加一道数字签名。我之前在某个项目里就是踩过这类的坑接口被人重放加篡改参数排查了很久才发现是缺少了应用层的完整性校验。所以这篇实战文章我直接分享一下如何用 Python 的 cryptography 库给 Flask 和 Django 的 API 加上数字签名确保请求从发出到落库的整个流程里任何一个字节被动过服务端都能第一时间识别出来。这篇文章适合谁看如果你在做前后端分离项目或者你在维护服务端到服务端的接口调用再或者你们公司正在设计对外开放的 API那这套签名方案可以直接借鉴。内容会覆盖签名原理、密钥对生成、签名与验签的核心代码、Flask 与 Django 的集成方式还有我实际踩过的坑和排查思路。不废话直接进入正题。1. 数字签名方案设计先搞清楚要防什么1.1 为什么不能只依赖 HTTPSHTTPS 很好用但要理解它的边界。HTTPS 用 TLS 协议加密了客户端和服务端之间的信道保证在网络上传输的报文是密文。这个保护在链路层面是有效的可一旦请求到达服务端之后很多事情就脱离了传输层的掌控。举个实际例子你的服务接入了一个统一的 API 网关网关要记录请求日志以便审计。日志系统通常会把整个请求体打印到文本文件或者采集进日志平台。这个日志如果被无关人员拿到里面的参数就被看光了其实签名解决的不是这个但更关键的问题是如果日志被改动了呢如果运维同学通过网关层调整了参数格式呢这些问题在传输层根本不会被感知。签名不一样签名基于业务参数本身。只要参数有任何变化签名立刻校验失败。它提供的是端到端的完整性保护。另外HTTPS 还有一层盲区如果客户端和服务端之间经过了企业内部的反向代理代理有时候为了做报文解析会对请求体做重组或字符转义这种转义并不会破坏 HTTPS 的证书链路但可能让服务端收到的字符跟客户端发出来的不一样。数字签名恰好能补上这一层。1.2 对称方案 HMAC 与非对称方案 RSA/ECDSA 怎么选设计签名方案时先要决定用的是对称密钥还是非对称密钥。对称方案的典型代表是 HMAC-SHA256也就是客户端和服务端共享一个 App Secret将请求参数按照约定的规则拼接用 Secret 作为密钥计算 HMAC 值服务端做同样的计算并比对。非对称方案则是用 RSA 或 ECDSA客户端持有私钥服务端只保存公钥。请求参数先做规范化序列化然后用私钥做签名运算服务端用公钥验证签名是否匹配。对称方案的优势是计算速度快、实现简单缺点是 Secret 一旦泄露攻击者可以随意签名任意请求而且 Secret 必须安全地存在两端的配置中心里。非对称方案的优势是私钥只在签名端服务端即便被拖库攻击者也拿不到私钥无法伪造签名缺点是计算开销更大、实现时需要注意的细节更多比如私钥的 PEM 加载方式、签名算法的 padding 模式。我给一个实战上的选型建议如果是内部系统之间的调用或者你的客户端也是你完全掌控的服务用 HMAC 完全够了简单高效。但如果是开放平台、合作方接入、或者客户端代码有可能被逆向拿到密钥的场景务必用 RSA 或者 ECDSA。下面这套实战方案我以 RSA 私钥签名、公钥验签的方式来写并且最后会补充 HMAC 的极简实现对比。1.3 数字签名到底防住了什么理解签名的作用最好的方式是明确威胁模型。加了数字签名之后你可以防住下面这几类攻击参数篡改中间人把订单金额从 100 改成 10000或者把收款账号换成自己的。签名一验分分钟失败。重放攻击攻击者截获一个合法请求在过期时间窗口内原样重发比如重复下单、重复扣款。配合时间戳和 nonce 之后就能挡住。伪装调用方没有私钥的人无法生成有效的签名无法假冒客户端。日志篡改后的误信如果服务端拿到的参数是日志里被改动过的验签直接失败不会继续执行业务。当然数字签名不是万能药。它不能防止私钥被盗不能替代访问控制也不负责数据的机密性加密。签名只保证完整性这个定位务必要清楚。2. 核心原理与 cryptography 基础先跑通签名验签2.1 数字签名的工作原理数字签名的过程可以分成两步。第一步是对内容做哈希摘要第二步是对摘要做非对称加密。客户端侧把所有业务参数按照约定规则序列化成一个字符串。使用 SHA-256 对字符串计算哈希。用 RSA 私钥对该哈希值进行加密得到签名。服务端侧收到请求后用同样规则序列化收到的业务参数。对序列化后的字符串计算 SHA-256 哈希。用公钥解签得到客户端签名时用的哈希值。比对服务端计算的哈希和解签后的哈希如果一致说明内容没被动过。生活化类比一下签名就像你在合同上盖的私章合同内容只要改了一个字你自己盖的章跟内容就对不上了权威机构一比对就知道这份合同被改过。2.2 cryptography 库的密钥对生成cryptography 是 Python 生态里目前最靠谱的密码学库之一比直接用 pycryptodome 要更规范、更贴近现代密码学标准。用它生成 RSA 密钥对很简单。from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization # 生成 2048 位 RSA 密钥对 private_key rsa.generate_private_key( public_exponent65537, key_size2048, ) # 获取私钥 PEM 格式 private_pem private_key.private_bytes( encodingserialization.Encoding.PEM, formatserialization.PrivateFormat.PKCS8, encryption_algorithmserialization.NoEncryption() ) # 获取公钥 PEM 格式 public_pem private_key.public_key().public_bytes( encodingserialization.Encoding.PEM, formatserialization.PublicFormat.SubjectPublicKeyInfo ) print(private_pem.decode(utf-8)) print(public_pem.decode(utf-8))这里强调几个点key_size 至少 20481024 已经不安全了4096 用在内部服务里性能也可接受。私钥格式我习惯用 PKCS8兼容性最好跨语言导入不折腾。公钥格式用 SubjectPublicKeyInfo也就是通常说的 X.509 的 SubjectPublicKeyInfo 编码OpenSSL 里用 PEM 导出的默认格式就长这样。NoEncryption 表示私钥文件本身不额外用口令加密。生产环境强烈建议加密或者放到专门的密钥管理系统里不落明文磁盘。2.3 签名与验签的标准姿势签名操作的代码def sign_message(private_key: bytes, message: str) - str: key serialization.load_pem_private_key( private_key, passwordNone ) signature key.sign( message.encode(utf-8), padding.PSS( mgfpadding.MGF1(hashes.SHA256()), salt_lengthpadding.PSS.MAX_LENGTH ), hashes.SHA256() ) return base64.b64encode(signature).decode(utf-8)验签操作的代码def verify_signature(public_key: bytes, message: str, signature_b64: str) - bool: key serialization.load_pem_public_key(public_key) try: key.verify( base64.b64decode(signature_b64), message.encode(utf-8), padding.PSS( mgfpadding.MGF1(hashes.SHA256()), salt_lengthpadding.PSS.MAX_LENGTH ), hashes.SHA256() ) return True except InvalidSignature: return False这里有个关键选择padding 用 PSS 还是 PKCS1v15PSS 是 RSA 签名里更安全的填充方案它在安全性证明上比 PKCS1v15 更让人放心。PKCS1v15 虽然兼容性极广、很多老系统还在用但它的构造存在一些理论上的攻击面。新项目我建议直接上 PSS如果跨语言对接只要对方支持 RSA-PSS 就行。但说句实在话现在主流语言和框架对 PSS 的支持已经非常普遍Java 的 Signature、Node.js 的 crypto、Go 的 rsa都能处理兼容性不用担心。另外一个细节PSS 的 salt_length 在两端的设置必须一致。我在实际项目里发现很多跨语言的问题就出现在这里。Java 端默认某些参数下 salt length 是 32Python 端如果用 MAX_LENGTH可能验签永不通过。稳妥起见签名和验签两端都固定写死为 32别用 MAX_LENGTH。我下面演示的代码就按这个约定来写。PSS_SALT_LENGTH 32 def sign_message(private_key: bytes, message: str) - str: key serialization.load_pem_private_key(private_key, passwordNone) signature key.sign( message.encode(utf-8), padding.PSS( mgfpadding.MGF1(hashes.SHA256()), salt_lengthPSS_SALT_LENGTH ), hashes.SHA256() ) return base64.b64encode(signature).decode(utf-8) def verify_signature(public_key: bytes, message: str, signature_b64: str) - bool: key serialization.load_pem_public_key(public_key) try: key.verify( base64.b64decode(signature_b64), message.encode(utf-8), padding.PSS( mgfpadding.MGF1(hashes.SHA256()), salt_lengthPSS_SALT_LENGTH ), hashes.SHA256() ) return True except InvalidSignature: return False这样最底层的签名验签基本能力就齐了。接下来要面对的是实际 API 请求的参数不是单个字符串而是一个 JSON 对象甚至带嵌套结构怎么规范化成被签名的内容。3. 参数规范化与签名生成这个环节最容易出兼容问题3.1 为什么一定要做参数规范化签名的核心动作是对一个字符串签名。如果客户端签名用的字符串和服务端验签用的字符串不一致哪怕业务参数完全一样签名都对不上。这个字符串怎么拼的规则就叫规范化是整套签名方案里最容易出坑的地方。最常见的坑是依赖字典序。请求参数在 HTTP 传输的时候是无序的客户端传 a1b2服务端收到之后解析成字典再遍历出来做签名时顺序可能就变成了 b2a1这样签名必挂。所以规范化的第一铁律把参数名按 ASCII 码升序排序后拼接。第二个坑是嵌套结构。JSON 里的对象和数组如果直接套 str() 或者 json.dumps()嵌套字典的 key 顺序在不同语言、不同库里可能都不一样生成的字符串完全不同。比如 {a:{x:1,y:2}}有的语言序列化出来是 {a:{x:1,y:2}}有的可能是 {a:{y:2,x:1}}。这一乱签名必挂。标准做法是自己实现一个确定性的序列化函数保证同样的 JSON 对象永远输出同样的字符串。3.2 常见规范方案对比我列三个实践中常用的方案方案一扁平化 keyvalue 拼接。只适用于参数是单层结构的情况。把所有基础类型参数按 key 字典序排序拼成 keyvaluekey2value2。嵌套参数直接不参与签名或者单独做序列化。实现最简单但遇到嵌套就只能放弃。方案二排序后的 JSON 序列化。把整个 JSON 结构递归排序字典 key 按字典序排列再用紧凑的 JSON 格式输出。这种方案通用性强嵌套结构完全支持。问题在于序列化时要考虑中文编码、特殊字符转义、浮点数精度等细节。方案三引入签名专用数据格式。例如先将 JSON 转成规范化的字符串表示采用 RFC 8785 这样的 JSON Canonicalization Scheme。这个方案最严谨但需要引入额外依赖。我的实际建议是内部系统用方案二就够了关键是序列化函数必须自己写一遍不要用现成的 json.dumps 直接上因为 sort_keys 参数只是让 key 排序没有处理字典里嵌套 key 的递归排序至少需要 ensure_asciiFalse 且 separators 固定。3.3 一个稳定可用的规范化实现我常用的是这个实现它来自我在几个项目里反复打磨过的版本核心思路就是递归对 dict 排序并统一分隔符import json def canonical_json(data) - str: return json.dumps( data, sort_keysTrue, ensure_asciiFalse, separators(,, :) )这个方法在大多数场景下是够用的但有一个隐患ensure_asciiFalse 之后中文会以 UTF-8 直接出现在字符串里。如果跨语言的另一端对非 ASCII 字符做了 unicode 转义可能就不一致。为了防止这种不确定我后来在跨语言对接的项目里改成了 ensure_asciiTrue 的统一转义形式效果更好因为 ASCII 转义是唯一确定性的写法。具体说改成这样def canonical_json(data) - str: return json.dumps( data, sort_keysTrue, ensure_asciiTrue, separators(,, :) )用 ensure_asciiTrue 以后所有非 ASCII 字符都会变成 \uXXXX 形式。这个做法最大的好处是什么不管两端用什么语言什么默认编码最终字符串完全一致不会因为编码问题出现明明签名算法一样却怎么都不通过的情况。但这里要提醒一句ensure_asciiTrue 对服务端 URL 传参方式有影响。因为 \uXXXX 一旦被 URL 编码\ 会变成 %5Cu 变成 %75。所以签名串最好放在请求头里而不是放进 URL query。如果一定放 query记得发送前对 header 签名值做 base64不要直接把 JSON 序列化塞进 URL。再看一眼完整流程payload { amount: 100, order_id: 20250101001, timestamp: 1735689600, nonce: 7f3b2e1a9c, } canonical_str canonical_json(payload) signature sign_message(private_key_pem, canonical_str)到这里签名串已经生成了。下一步就是如何把这个东西挂到 Flask 和 Django 的请求链路上让每个请求自动校验。4. Flask 与 Django 集成实战把签名校验做成通用能力4.1 先约定请求头的传递格式不管是 Flask 还是 Django签名相关的信息我建议都放在请求头里不要污染业务参数。具体约定如下X-Signature: 签名的 Base64 字符串X-Timestamp: 客户端当前时间的 Unix 时间戳X-Nonce: 随机字符串每次请求唯一用于防重放时间戳和 nonce 为什么不能省原因很简单单纯签名只能防篡改不能防重放。攻击者拿到一个合法请求只要签名本身有效他可以在业务规则允许的时间窗口内重复提交。时间戳给了服务端一个判断请求是否过期的依据nonce 则是为了进一步防止攻击者在时间窗口内重放同一请求。服务端校验顺序是先取时间戳判断是否在允许的偏差范围内比如前后 300 秒再去查 nonce 是否用过这个需要存在 Redis 或数据库里最后才验签。顺序不能乱因为验签是 CPU 密集操作如果请求已经过期就直接拒绝没必要浪费算力去验签。4.2 Flask 的 before_request 全局校验Flask 里最简单的做法是注册一个 before_request 钩子。这个方法在 Flask 里会被所有视图函数共享不管路由写到哪里都会先执行。import time import base64 from flask import Flask, request, jsonify app Flask(__name__) PUBLIC_KEY_PEM b-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY----- VALID_WINDOW 300 def verify_signature_from_headers(headers, body): signature headers.get(X-Signature, ) timestamp headers.get(X-Timestamp, ) nonce headers.get(X-Nonce, ) if not signature or not timestamp or not nonce: return False, missing signature headers if abs(time.time() - int(timestamp)) VALID_WINDOW: return False, timestamp expired # nonce 检查这里假设你有一个 RedisClient先略过 # if redis_client.get(fnonce:{nonce}): # return False, nonce already used # redis_client.setex(fnonce:{nonce}, VALID_WINDOW, 1) try: body_data request.get_json() except Exception: body_data {} sign_data { timestamp: timestamp, nonce: nonce, body: body_data, } canonical_str canonical_json(sign_data) if not verify_signature(PUBLIC_KEY_PEM, canonical_str, signature): return False, signature invalid return True, ok app.before_request def check_signature(): if request.path.startswith(/public): return None ok, msg verify_signature_from_headers(request.headers, request.get_data().decode(utf-8)) if not ok: return jsonify({code: 401, msg: msg}), 401 return None注意这里有个细节我把 timestamp、nonce、body 三个一起放进待签名的数据结构里而不是只对 body 签名。这样做有什么好处首先如果只对 body 签名timestamp 和 nonce 就可以被攻击者随意改过期重放就防不住了。其次把 timestamp 和 nonce 纳入签名可以防止攻击者把旧请求的 body 配到新时间戳里再签一次——没有私钥根本签不出来。所以实际项目里签名范围一定是请求元信息 业务参数的整体不是只对 body。4.3 Django 中间件实现方式Django 的做法是写一个自定义中间件。Django 中间件的执行顺序是从上往下、再返回所以通常在 settings 的 MIDDLEWARE 列表里至少在 session 和 auth 中间件之前加入签名校验中间件这样越早拦截越好。中间件代码import time from django.http import JsonResponse class SignatureCheckMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): # 跳过某些公开接口 if request.path.startswith(/healthz): return self.get_response(request) signature request.headers.get(X-Signature, ) timestamp request.headers.get(X-Timestamp, ) nonce request.headers.get(X-Nonce, ) if not all([signature, timestamp, nonce]): return JsonResponse({code: 401, msg: missing signature headers}, status401) try: if abs(time.time() - int(timestamp)) 300: return JsonResponse({code: 401, msg: timestamp expired}, status401) except ValueError: return JsonResponse({code: 401, msg: invalid timestamp}, status401) # nonce 防重放检查略建议放 Redis # if not check_nonce(nonce): # return JsonResponse({code: 401, msg: nonce used}, status401) try: body_data json.loads(request.body.decode(utf-8)) if request.body else {} except json.JSONDecodeError: return JsonResponse({code: 401, msg: invalid json body}, status401) sign_data { timestamp: timestamp, nonce: nonce, body: body_data, } canonical_str canonical_json(sign_data) if not verify_signature(PUBLIC_KEY_PEM, canonical_str, signature): return JsonResponse({code: 401, msg: signature invalid}, status401) # 通过后把验签解析出的信息挂到 request 上方便后续视图使用 request.signed_params body_data return self.get_response(request)需要提醒的是Django 在收到 request.body 之后如果后续中间件或视图还要读 bodyRequestBody 在 Django 中默认只能读一次。为了让视图还能读 body可以在读过之后把 body 重新赋值或者自定义一个缓存 Body 的中间件。我在项目里的做法是在中间件中读取 request.body 并缓存到 request._body_cache然后在后续视图读取时使用 request.body 依然能拿到。具体实现要看 Django 版本新版 Django 的 request.body 本身支持多次读取问题不大。4.4 视图层如何使用验签后的参数不管 Flask 还是 Django验签通过之后后续视图就不要再信任外部直接传来的参数了而应该信任经过签名校验后的参数。在实际代码中我是这么做的Flask 视图里直接读 request.json因为前置步骤已经确认它没被篡改。如果想要更严格可以把校验后的参数放到 flask.g 或 request 的某个扩展属性上视图只从那里取值。Django 的中间件里已经把 body 放到了 request.signed_params视图就统一用 request.signed_params.get(amount) 这样的方式取值。这样做的意义是视图开发人员无需关注签名逻辑只要在中间件之后的链路拿到的就是可信参数。这算是一个约定优于配置的实践。5. 客户端签名示例与支付级安全细节5.1 Python 客户端如何构造签名请求服务端写完客户端的实现也得跟上。以 Python requests 为例客户端流程是import time import uuid import json import requests import base64 PRIVATE_KEY_PEM open(private.pem, rb).read() def build_signed_request(private_key_pem, body: dict): timestamp str(int(time.time())) nonce uuid.uuid4().hex sign_data { timestamp: timestamp, nonce: nonce, body: body, } canonical_str canonical_json(sign_data) signature sign_message(private_key_pem, canonical_str) headers { X-Signature: signature, X-Timestamp: timestamp, X-Nonce: nonce, Content-Type: application/json, } return headers, json.dumps(body, ensure_asciiFalse) headers, payload build_signed_request(PRIVATE_KEY_PEM, {amount: 100, order_id: 123}) resp requests.post(https://api.example.com/v1/order, datapayload, headersheaders)这个客户端代码用到了我们上面定义的 canonical_json 和 sign_message。如果客户端不是 Python而是 Java、Go、Node.js核心就是把这个 normalized string 和 PSS 参数对齐其他逻辑完全一样。5.2 密钥管理比签名算法更值得上心的事签名算法本身不是安全短板真正容易出问题的是密钥管理。我经常看到团队把私钥文件放在项目仓库里或者写死在配置文件中这是非常危险的。私钥建议存放在专门的密钥管理服务中比如云上的 KMS或者自建的 Vault。应用启动时通过 SDK 获取私钥内存中解密使用磁盘上不落明文。至少要做到私钥文件权限 600只允许运行用户读取仓库 .gitignore 排除密钥文件开发环境、测试环境、生产环境使用不同的密钥对。定期轮换密钥。我所在的开发小组是每半年轮换一次轮换期间同时支持两套公钥旧公钥保留一个过渡期。公钥虽然不算机密但公钥的传递也需要防篡改否则攻击者替换公钥后就能伪造签名了。公钥尽量通过配置中心或代码仓库里统一管理。必要场景下可以用另一套机制为公钥做签名认证比如直接用 HTTPS 从配置中心拉取。5.3 重放攻击与防重放机制防重放这块我再展开说。时间戳窗口校验是最基础的但只是限制了攻击窗口。nonce 机制可以在这个窗口内彻底防住同一请求重放。实现上可以把 nonce 写入 Redis设置过期时间跟时间戳窗口一致。import redis r redis.Redis(hostlocalhost, port6379, db0) def check_and_store_nonce(nonce: str, window: int) - bool: key fnonce:{nonce} if r.set(key, 1, nxTrue, exwindow): return True return False如果请求处理速度极快同一个 nonce 在窗口期内重复提交第二次就会命中 Redis 已有键直接拒绝。这里有个生产级细节Redis 的 set 需要带 nxTrue 才能保证只有第一次写入成功这个操作是原子的在高并发下不会出现两个请求同时通过校验的情况。nonce 的生成建议用 UUID4 或者 16 字节以上的随机数保证不可预测。不要用自增 ID、时间戳这类可预测的值否则攻击者可以预测并构造重复组合。6. 常见问题排查与我的经验总结6.1 跨语言验签失败的七大原因我在实际对接中遇到最多的问题就是客户端签名服务端验签就是不通过。这里列出最常见的七个原因salt_length 不一致。Java 端有些库默认 PSS salt length 是 32Python 端如果用了 MAX_LENGTH两端就对不上了。待签名字符串不一致。这是排名第一的原因。常见于字典顺序、嵌套 JSON key 顺序、中文字符串编码、URL 编码方式不一致。签名值做了 URL 编码。如果签名值是 Base64Base64 里的 、/、 字符在 URL 传输时会被转义服务端接收后必须做反转义才能 base64 解码。公钥格式解析失败。很多人用 Java 的 X509EncodedKeySpec 生成公钥Python 端load_pem_public_key 可能不支持需要转换格式。Base64 解码时出现错误。签名值的 padding 被截断或者被错误地去掉导致二进制不一致。请求体为空的情况。客户端签的是空 body {}服务端解析 request.get_json() 拿到 None两者序列化结果不一致。时间戳边界问题。客户端生成签名用的 timestamp 和服务端验签时读到的 timestamp 不一致。细心看代码会发现如果你把 timestamp 放在 header 里签名时也用了这个 timestamp那两边只要都用同一个 header 里的 timestamp这块反而不会出错。针对这七点调试套路也很固定先在客户端和服务端分别打印出待签名的 canonical string比对这两个字符串是否完全一致。只要这两个字符串一致签名百分之百能通过不一致就把差异点找出来。这个排查步骤在跨语言对接时能节省大量时间。6.2 性能优化与请求体大小控制签名运算虽然是 CPU 密集操作但 RSA 验签一次在普通机器上大约只需要几毫秒正常情况下没问题。但如果在高并发接口上每个请求都走 RSA 验签再加上 PSS 的 salt length 较大性能压力会显现出来。建议的做法网关层做验签应用层只做业务处理。验签放在 Nginx 的 Lua 脚本、Kong、APISIX 这类网关上可以把计算压力从业务服务中剥离出去。如果不需要非对称能力内部服务改走 HMAC-SHA256验签速度快一个数量级代码也更简单。限制请求体大小。签名校验通常需要在拿到完整 body 之后进行如果请求体没有上限攻击者可以发超大 body 把你 CPU 打满。建议在网关或应用层把 body 大小限制在 1MB 左右。6.3 HMAC 极简替代方案如果你的场景完全不需要非对称就用 HMAC-SHA256 加 App Secret 的方式。代码比 RSA 简单得多import hmac import hashlib import base64 def generate_hmac_signature(secret: str, canonical_str: str) - str: signature hmac.new( secret.encode(utf-8), canonical_str.encode(utf-8), hashlib.sha256 ).digest() return base64.b64encode(signature).decode(utf-8)服务端验签就是重新计算一次然后比对def verify_hmac(secret: str, canonical_str: str, signature: str) - bool: expected generate_hmac_signature(secret, canonical_str) # 恒定时间比较防止时序攻击 return hmac.compare_digest(expected, signature)注意 hmac.compare_digest 这个函数绝不能替换成 比较。用 的话字符串长度和内容可以在时间上产生可观测差异存在时序攻击风险。这是对称方案的加分项务必写上。6.4 我对这套方案的心得最后聊点我的实际体会。项目里我用这套 RSA 签名方案接过支付回调、开放平台 API、服务间消息调用甚至在某个金融场景的接口里验证过。回头看最大的教训就一个签名方案设计得再好如果规范化规则不严谨最后全都白搭。签名算法的选型、密钥的存储、防重放机制这些都是在规范化稳定之后才能发挥作用的。另外有一个容易被忽视的点日志记录。验签失败的请求一定要记录完整的 header 信息和待签名字符串可以脱敏这对线上问题排查非常关键。我遇到过生产环境某个客户端突然验签失败就是因为对方序列表里多了一个空字段而我们这边把空字段过滤掉了。没有日志这种问题根本无从查起。如果你现在就准备动手改造自己的 API我建议按这个顺序来先写一个独立的签名服务模块把所有生成、校验逻辑收敛到这一个模块里再在 Flask 和 Django 中接入中间件最后写配套的客户端 SDK。一次封装两端复用。等到你的 API 开始接入外部合作方或者面向更复杂的网络环境时你会发现这套签名机制带来的价值远不止是请求不被篡改这么简单。它是你的服务从能跑走向可信的关键一步。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑