惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tor Project blog
AWS News Blog
AWS News Blog
量子位
博客园 - 聂微东
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
人人都是产品经理
人人都是产品经理
A
About on SuperTechFans
L
Lohrmann on Cybersecurity
Know Your Adversary
Know Your Adversary
V
Visual Studio Blog
P
Privacy International News Feed
K
Kaspersky official blog
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
GRAHAM CLULEY
S
Securelist
Last Week in AI
Last Week in AI
Latest news
Latest news
B
Blog
T
Tenable Blog
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
T
The Exploit Database - CXSecurity.com
N
Netflix TechBlog - Medium
A
Arctic Wolf
SecWiki News
SecWiki News
C
Cisco Blogs
月光博客
月光博客
PCI Perspectives
PCI Perspectives
Cloudbric
Cloudbric
H
Hacker News: Front Page
F
Full Disclosure
TaoSecurity Blog
TaoSecurity Blog
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Schneier on Security
罗磊的独立博客
S
Security Affairs
Scott Helme
Scott Helme
P
Proofpoint News Feed
GbyAI
GbyAI
Google Online Security Blog
Google Online Security Blog
The Register - Security
The Register - Security
W
WeLiveSecurity
Hugging Face - Blog
Hugging Face - Blog
博客园 - 叶小钗

博客园 - 星河赵

LangChain AI 客服 Agent 项目学习笔记 海外stripe 聚合支付 ffmpeg 介绍 AE中制作带可替换屏幕的视频模版 记录|AI超写实短视频制作流程+提示词 服务端部署Vue 在Pycharm 中使用 Python Module 方式启动 uvicorn Conda 虚拟环境完整指南 CentOS / OpenCloudOS 服务器如何安装远程桌面 Python 项目模块导入问题解决方案 2026 谷歌 Antigravity 最新安装教程! Vscode 常用配置 如何修改 Redis 数据存放路径 Vue 后台项目 Nginx 部署笔记(SPA / Vite 构建) Python -m 用法(极简版) Ubuntu 上安装 MongoDB 并启用事务的完整流程 mac 微信双开新方法 nginx 对静态资源进行压缩 python fast api websocket 连接事例 在 Flask 中并发执行任务 Vscode 配置快捷键和JetBrains(pycharm)一样 Linux 服务器的 SSH 登录端口号 从默认的 22 改掉 配置 Docker 镜像加速器 python fast api 部署
HMAC-SHA256 请求签名与验签实践(Python 可直接复用)
星河赵 · 2025-10-17 · via 博客园 - 星河赵
  • 目标:沉淀一套“能复制即用”的签名/验签规范与代码,解决接口防篡改与防伪造。
  • 关键规则:
    • 待签名串:METHOD + "\n" + Content-MD5 + "\n" + URI_PATH + [ "?" + sorted_query ]
    • Content-MD5:对“原始请求体字节”计算 MD5,hex 小写
    • Query:对 key 做升序排序;同 key 多值取第一个;拼接为 k=v&k2=v2
    • 签名:HMAC-SHA256(secret_bytes, utf8(string_to_sign)) → hex 小写

签名规范

  • 待签名串
    • METHOD 使用大写,如 POST、GET
    • URI_PATH 仅路径,不包含协议/域名/端口,例如 /open_api/query/template
    • 若无 query,省略 ? 部分
  • Content-MD5
    • 取“原始 body 字节”做 MD5;空 body 视为空字节
    • JSON 场景需使用“紧凑 JSON”序列化:separators=(',', ':'), ensure_ascii=False,UTF-8 编码
  • Query 处理
    • key 升序;多值取第一个;不做 URL 编码二次处理(与客户端保持一致)
  • 签名算法
    • HMAC-SHA256,密钥为字节串,输出十六进制小写

可直接复用代码(签名 + 验签)

import hmac
import hashlib
import json
from typing import Dict, Iterable, Tuple, Union, Optional
from urllib.parse import urlparse, parse_qs

ArgsType = Optional[Dict[str, Union[str, Iterable[str]]]]

def _compact_json_bytes(body: Optional[dict]) -> bytes:
    """
    将字典转为“紧凑 JSON”UTF-8字节;None/空字典→空字节
    """
    if not body:
        return b""
    raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
    return raw.encode("utf-8")

def _sorted_query(args: ArgsType) -> str:
    """
    将 query 参数按 key 升序拼接为 k=v&k2=v2。多值取第一个。
    None 或空返回 ""。
    """
    if not args:
        return ""
    keys = sorted(args.keys())
    pairs = []
    for k in keys:
        v = args[k]
        if isinstance(v, (list, tuple, set)):
            v = list(v)[0] if v else ""
        pairs.append(f"{k}={v}")
    return "&".join(pairs)

def build_string_to_sign(
    http_method: str,
    uri_path: str,
    args: ArgsType,
    body_bytes: bytes,
) -> Tuple[str, str]:
    """
    构造待签名串与 content_md5(hex 小写)
    返回: (string_to_sign, content_md5)
    """
    method = http_method.upper()
    content_md5 = hashlib.md5(body_bytes).hexdigest()
    qs = _sorted_query(args)
    uri_and_qs = f"{uri_path}?{qs}" if qs else uri_path
    string_to_sign = f"{method}\n{content_md5}\n{uri_and_qs}"
    return string_to_sign, content_md5

def generate_signature(
    http_method: str,
    uri_path: str,
    args: ArgsType,
    body: Optional[dict],
    secret: Union[str, bytes],
) -> Tuple[str, bytes, str]:
    """
    生成签名
    返回: (signature_hex, body_bytes, string_to_sign)
    """
    if isinstance(secret, str):
        secret = secret.encode("utf-8")

    body_bytes = _compact_json_bytes(body)
    string_to_sign, _ = build_string_to_sign(http_method, uri_path, args, body_bytes)
    signature = hmac.new(secret, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
    return signature, body_bytes, string_to_sign

def verify_signature(
    http_method: str,
    uri_path: str,
    args: ArgsType,
    raw_body_bytes: bytes,
    provided_signature_hex: str,
    secret: Union[str, bytes],
) -> bool:
    """
    验签。注意 raw_body_bytes 必须是请求体原始字节(未二次序列化)。
    """
    if isinstance(secret, str):
        secret = secret.encode("utf-8")
    string_to_sign, _ = build_string_to_sign(http_method, uri_path, args, raw_body_bytes)
    expected = hmac.new(secret, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
    # 常量时间比较,避免时序侧信道
    return hmac.compare_digest(provided_signature_hex, expected)

客户端请求示例(Python requests,可直接复制)

import os
import requests
from urllib.parse import urljoin, urlparse, parse_qs

# 引入上方的 generate_signature
# from sign_utils import generate_signature

BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
URI_PATH = "/open_api/query/template"
URL = urljoin(BASE_URL, URI_PATH)

APP_ID = os.getenv("MY_APP_ID", "your_app_id_here")
SECRET = os.getenv("MY_SECRET", "your_secret_here")  # 建议使用环境变量配置

body = {"template_id": "your_template_id"}

# 解析 query(若 URL 上还有 ?k=v,可从这里带入参与签名)
args = parse_qs(urlparse(URL).query)

sign, body_bytes, string_to_sign = generate_signature(
    http_method="POST",
    uri_path=URI_PATH,
    args=args,
    body=body,
    secret=SECRET,
)

headers = {
    "WX-SIGN": sign,
    "WX-APPID": APP_ID,
    "Content-Type": "application/json; charset=utf-8",
    # "WX-DEBUG": "true",  # 调试时可开启
}

# 以原始字节发送,确保与签名一致
resp = requests.post(URL, data=body_bytes, headers=headers, timeout=10)
print("status:", resp.status_code)
print("resp:", resp.text)
# 调试输出
print("string_to_sign:", repr(string_to_sign))