测小狮实战 Day 21:Python Requests 搞定 POST/加密签名/文件上传 —— GET 之后你该会的那些接口
兄弟们,GET 请求你搞定了,但真正难搞的接口才刚刚开始。
上篇咱们聊了 GET 请求怎么发、参数怎么带、响应怎么解析,用的都是最基础的 requests.get()。
但实际工作中,80% 的核心接口都是 POST——登录要 POST 密码、支付要 POST 订单、上传文件要 POST 二进制流、调用开放平台要 POST 签名参数……
这些场景,GET 那套可搞不定。今天咱们就把 POST 进阶三件套:JSON 请求、加密签名、文件上传彻底讲透,附完整代码 + 踩坑实录 + 面试 Q&A。
📌 本文你将学到:POST 三种数据格式(form/json/files) → MD5/SHA256/HMAC 签名原理与实战 → 时间戳+nonce 防重放 → multipart/form-data 文件上传完整流程 → 5 个踩坑调试技巧 → 面试 Q&A
一、POST 到底哪里不一样?
先说本质区别:GET 是"拿数据",POST 是"交数据"。GET 把参数塞 URL 里,POST 把参数放请求体(body)里。
但更关键的是——POST 请求体有多种格式,服务端必须知道你在用哪一种:
💡 测小狮有话说:POST 的 Content-Type 决定了服务端怎么解析你的 body。一旦搞混,轻则拿不到数据,重则 415 Unsupported Media Type 报错。
三种最常见 Content-Type:
• application/x-www-form-urlencoded:表单编码,原始 WEB 表单默认格式• application/json:JSON 编码,现代 API 最爱用• multipart/form-data:多部件格式,支持文件二进制+文本混合
二、POST 请求基础:表单 / JSON / 文件
咱们分别来看 requests.post() 的三种写法:
① 表单提交(application/x-www-form-urlencoded)
最传统的表单提交方式,用户登录、搜索提交都常见:
import requests # 用 data= 参数,默认 Content-Type 就是 application/x-www-form-urlencoded payload = { "username": "test_user", "password": "Test@1234", "remember": "on" } resp = requests.post( "https://api.example.com/login", data=payload, # data= 自动做 urlencoded timeout=10 ) print(resp.status_code) print(resp.json())
🪤 踩坑提醒:data=payload 和 json=payload 长得很像,但服务端处理逻辑完全不同。如果你用 data= 发 JSON 格式的字符串,服务端收不到正确的 JSON。务必确认接口文档要求的格式。
② JSON 提交(application/json)
现代 API 最推荐的格式,结构清晰、支持嵌套:
import requests import json payload = { "user_id": 12345, "order": { "item": "VIP年卡", "price": 299.00, "coupon": "NEWUSER2026" }, "tags": ["新用户", "高价值"] } resp = requests.post( "https://api.example.com/create_order", json=payload, # 自动序列化 + 自动设置 Content-Type: application/json timeout=10 ) print(resp.status_code) print(resp.json())
核心区别总结:
• requests.post(url, json=data) → Python dict 自动序列化为 JSON 字符串,Header 自动设 Content-Type: application/json• requests.post(url, data=data) → 表单格式提交,Header 自动设 Content-Type: application/x-www-form-urlencoded• requests.post(url, json=json_str) → 如果传的是字符串,不会二次序列化,直接发出去
💡 测小狮有话说:如果你遇到接口一直报 400 Bad Request,但 Postman 能通,先 F12 抓一下 Postman 实际发的请求头,对比一下你的 Content-Type 和 Accept 是否一致。
三、加密签名:开放平台 API 的"身份证"
GET 搞定之后,很多同学第一次碰签名接口就蒙了——为什么要签名?
原因很简单:防止请求被篡改和重放攻击。你传一个"转账 100 元"的请求,攻击者截获后改成"转账 10000 元"再发出去,没有签名验证服务端根本不知道被改了。
签名主流算法:MD5 / SHA256 / HMAC。咱们从实战角度逐个讲:
① MD5 签名(简单,已不推荐用于安全场景)
MD5 已经被王小云教授证明可碰撞,不适合安全性要求高的场景,但很多老系统内部还在用:
import hashlib def md5_sign(params_dict, secret_key): """ MD5 签名:把所有参数按 key 字母序拼接 + secret_key,再 MD5 """ # 1. 按 key 字母序排序 sorted_keys = sorted(params_dict.keys()) # 2. key=value&key=value 拼接 sign_str = "&".join([f"{k}={params_dict[k]}" for k in sorted_keys]) # 3. 末尾加上密钥 sign_str += f"&key={secret_key}" # 4. MD5 并转大写 hex return hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper() # 示例参数 params = { "amount": "100.00", "order_no": "ORD20260807001", "timestamp": "1723027200", "user_id": "u_8888" } # 实际密钥从配置读取,不要硬编码 app_secret = "your-app-secret" signature = md5_sign(params, app_secret) print("签名结果:", signature) # 完整签名原文:amount=100.00&order_no=ORD20260807001×tamp=1723027200&user_id=u_8888&key=your-app-secret
② SHA256 签名(目前主流,推荐使用)
SHA256 比 MD5 安全性高很多,微信支付、支付宝、多数开放平台都用它:
import hashlib import time def sha256_sign(params_dict, secret_key): """ SHA256 签名(按各平台文档实现): 1. 按 key 字母序排序 2. key=value 拼接,用 & 连接 3. 末尾拼接 key=密钥 4. SHA256 → hex → 转大写 """ sorted_keys = sorted(params_dict.keys()) # 过滤空值(部分平台要求不参与签名) pairs = [f"{k}={params_dict[k]}" for k in sorted_keys if params_dict[k] != ""] sign_str = "&".join(pairs) + f"&key={secret_key}" return hashlib.sha256(sign_str.encode("utf-8")).hexdigest().upper() # 实战:带时间戳防重放 params = { "app_id": "wx_your_app_id", "nonce_str": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS", # 随机字符串 "timestamp": str(int(time.time())), # 时间戳(秒) "sign_type": "SHA256", "body": "VIP会员-年卡", "total_fee": "299", "trade_type": "APP" } app_secret = "your-app-secret" signature = sha256_sign(params, app_secret) params["sign"] = signature # 把签名加进参数 print("完整签名参数:", params)
③ HMAC-SHA256(更安全的变体,用于要求更严格的平台)
HMAC 比普通 Hash 多了一层密钥混淆,安全性更高:
import hmac import hashlib import secrets import time def hmac_sha256_sign(message, secret_key): """ HMAC-SHA256 签名: message: 待签名的原始字符串 secret_key: 密钥(各平台提供) 返回:大写十六进制签名 """ return hmac.new( secret_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256 ).hexdigest().upper() # 生成随机 nonce(每次请求必须不同,防止重放) def generate_nonce(): return secrets.token_hex(16) # 32位随机字符串 # 示例:某开放平台签名方式 message = "GET&%2Fv3%2Forder%2Fquery&app_id%3Dwx_your_app_id%26nonce_str%3D5K8264%26timestamp%3D1723027200" app_secret = "your-app-secret&" signature = hmac_sha256_sign(message, app_secret) print("HMAC-SHA256:", signature)
🪤 踩坑提醒:签名踩坑三连——① 排序范围搞错(有些平台不含 sign 本身、有些不含空值);② 编码格式不一致(GBK vs UTF-8);③ 时间戳用了本地时间而不是 UTC。调试时一定要拿到平台提供的「签名验证工具」做对比。
四、文件上传:multipart/form-data 实战
很多同学觉得文件上传很神秘,其实打开来看就是特殊的 HTTP multipart body 格式。requests 帮你封装好了,自己写也不用怕。
① 单文件上传(Excel报表)
import requests # 打开文件(注意:文件对象要支持 .read()) with open("user_report.xlsx", "rb") as f: files = { # 字段名: (文件名, 文件二进制, MIME类型) "file": ("user_report.xlsx", f, "application/vnd.ms-excel") } data = { "upload_type": "daily_report", "report_date": "2026-08-07", "operator": "test_user" } resp = requests.post( "https://api.example.com/upload/report", files=files, data=data, # 非文件字段用 data= timeout=30 ) print(resp.json())
MIME 类型参考:
• Excel 旧版 .xls:application/vnd.ms-excel• Excel 新版 .xlsx:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet• CSV:text/csv• PNG 图片:image/png• JSON 配置:application/json
② 多文件同时上传(一次传多个附件)
import requests # 一次上传多个文件,files 传 list with open("data.csv", "rb") as f1, open("config.json", "rb") as f2: files = [ ("files", ("data.csv", f1, "text/csv")), ("files", ("config.json", f2, "application/json")) ] resp = requests.post( "https://api.example.com/upload/batch", files=files, timeout=60 ) print(resp.json())
🪤 踩坑提醒:用 files={} 形式上传时,一定不要提前把文件读进内存然后传 bytes——因为某些服务端会多次读取 body 验证签名。正确做法是传文件对象,让 requests 的 MultipartEncoder 管理流的读取节奏。
五、实战:一个完整的带签名文件上传流程
理论讲完了,来一个完整的企业级实战——某开放平台的文件上传接口,要求:
• POST multipart/form-data 上传文件• 参数包括业务字段 + 签名(防篡改)+ 时间戳 + nonce(防重放)• 签名算法:SHA256,按 key 字母序拼接
import requests import hashlib import time import secrets import os # ========== 签名工具函数 ========== def build_signature(params_dict, app_secret): """ 按 key 字母序拼接参数,SHA256,转大写 hex """ sorted_keys = sorted(params_dict.keys()) pairs = [f"{k}={params_dict[k]}" for k in sorted_keys] sign_str = "&".join(pairs) + f"&key={app_secret}" return hashlib.sha256(sign_str.encode("utf-8")).hexdigest().upper() def upload_file_with_signature( file_path, url="https://api.example.com/v2/file/upload", app_id="wx_your_app_id", app_secret="your-app-secret", biz_type="test_report" ): """ 带签名文件上传完整流程 """ # 1. 检查文件存在 if not os.path.exists(file_path): raise FileNotFoundError(f"文件不存在: {file_path}") # 2. 生成时间戳 + 随机nonce(防重放关键) timestamp = str(int(time.time())) # 秒级时间戳 nonce = secrets.token_hex(16) # 32位随机字符串,每次不同 # 3. 构造业务参数(不含文件) params = { "app_id": app_id, "biz_type": biz_type, "file_name": os.path.basename(file_path), "nonce_str": nonce, "timestamp": timestamp } # 4. 生成签名 sign = build_signature(params, app_secret) params["sign"] = sign # 5. 打开文件,准备 multipart file_size = os.path.getsize(file_path) with open(file_path, "rb") as f: files = { "file": ( os.path.basename(file_path), f, "application/vnd.ms-excel" # .xlsx 用 vnd.openxmlformats... ) } # 6. 发请求:files + 文本参数(data=) resp = requests.post( url, files=files, data=params, # 签名参数走 data,不走 files timeout=60 ) return resp.json() # ========== 调用示例 ========== if __name__ == "__main__": result = upload_file_with_signature( file_path="/tmp/test_report.xlsx", biz_type="daily_upload" ) print("上传结果:", result) # 典型返回: {"code": 0, "msg": "success", "file_id": "file_xxx"}
整个流程的核心逻辑:
参数签名 → 生成 nonce + timestamp → 拼装 multipart → 发送 → 验签成功 → 文件落地
这整套逻辑在企业里是标准模板,搞定这一个,80% 的带签名上传接口都能应付。
💡 测小狮有话说:上面的代码还有个隐藏问题——file_name 如果参与签名,但上传时文件名被服务端改了(比如加了时间戳),验签就会失败。很多平台为此会要求签名时用原始文件名,而不是服务端返回的文件名。
六、踩坑与调试技巧
🪤 坑1:请求成功了但服务端说"签名错误"
调试方法:拿到接口文档里的「测试密钥」和「示例请求」,用同样的参数自己算一遍签名,对比 hex 是否完全一致。
# 用日志把签名原文打出来,一步步对比 def debug_signature(params_dict, app_secret): sorted_keys = sorted(params_dict.keys()) pairs = [f"{k}={params_dict[k]}" for k in sorted_keys] sign_str = "&".join(pairs) + f"&key={app_secret}" print("[DEBUG] sign_str =", sign_str) print("[DEBUG] sha256 =", hashlib.sha256(sign_str.encode("utf-8")).hexdigest().upper())
🪤 坑2:文件上传超时,但文件明明不大
原因:默认 timeout=30 对于大文件或弱网不够。另一个原因:files= 和 data= 混用时,requests 会先完整读取所有文件再计算 Content-Length,大文件很慢。
🪤 坑3:文件上传后服务端拿不到参数
检查:是不是把带签名的参数也放进了 files=?正确的做法是——文件放 files=,签名参数放 data=,两者不要混在一起。
🪤 坑4:POST 后一直等响应,线程卡死
设置合理的 timeout,同时用 Session() 对象复用连接,提高并发性能:
import requests # 复用 TCP 连接,适合批量请求 session = requests.Session() session.headers.update({ "User-Agent": "TestAuto/1.0", "Accept": "application/json" }) for i in range(100): resp = session.post(url, json=payload, timeout=10) assert resp.json()["code"] == 0 session.close() # 记得关闭
🪤 坑5:签名里的时间戳和服务器时间差太大(±5分钟)
有些平台要求时间戳误差 <5 分钟否则拒绝。解决办法:先调一次「获取服务器时间」接口,计算本地与服务器的时钟差,之后所有请求用「本地时间 + 时钟差」作为 timestamp。
七、面试 Q&A
Q1:POST 的 data= 和 json= 区别是什么?什么场景用哪个?
本质区别在于请求体的格式和 Header 的 Content-Type。data= 发表单格式(URL-encoded),json= 发 JSON 格式(序列化为 JSON 字符串)。表单提交用 data=,API 接口用 json=。如果两者混用,服务端会报 400 或拿到 null。
Q2:GET 请求能不能带 body?
技术上 HTTP 规范没有禁止,但实际中大多数服务端和代理服务器会忽略 GET 请求的 body,或者直接报错。微信支付等平台甚至强制要求 GET 请求不带 body。所以 GET 就是 GET,别想用它传 JSON。
Q3:MD5 和 SHA256 签名有什么区别?为什么现在主流用 SHA256?
MD5 输出 128 bit(32 hex),SHA256 输出 256 bit(64 hex),后者抗碰撞强度高得多。MD5 已被王小云教授证明可在短时间内找到碰撞,用于金融/支付等安全场景是严重违规。SHA256 目前没有公开的有效攻击方法,主流开放平台(微信/支付宝/抖音)均采用。
Q4:nonce 和 timestamp 为什么必须每次不同?
防止重放攻击。假设攻击者截获了你合法的"转账请求",如果不验证 nonce,攻击者直接重发一遍就能反复转账。nonce(随机数)+ timestamp(时间戳)组合让每次请求都独一无二,且服务端可拒绝时间窗口外的重复请求(比如 5 分钟前的请求)。
Q5:文件上传时,文件名乱码怎么办?
很多老平台不支持 UTF-8 文件名,会导致文件名变 ???.xlsx。解决方案:requests 支持手动指定 filename 的编码:
from requests_toolbelt import MultipartEncoder encoder = MultipartEncoder( fields={ "file": ("测试报告.xlsx", open("report.xlsx", "rb"), "application/vnd.ms-excel"), "biz_type": "daily_report" }, boundary="----WebKitFormBoundary7MA4YWxkTrZu0gW" ) # 手动指定文件名编码 resp = requests.post( url, data=encoder, headers={"Content-Type": encoder.content_type} )
Q6:如何批量测试 POST 接口的性能和稳定性?
import requests import time from concurrent.futures import ThreadPoolExecutor, as_completed url = "https://api.example.com/create_order" payload = {"item_id": "VIP_001", "price": 299} def post_order(i): start = time.time() r = requests.post(url, json=payload, timeout=10) return i, r.status_code, r.json().get("code"), time.time() - start # 100 并发,1000 总请求 with ThreadPoolExecutor(max_workers=100) as pool: futures = [pool.submit(post_order, i) for i in range(1000)] success, fail, costs = 0, 0, [] for f in as_completed(futures): idx, code, resp_code, cost = f.result() if code == 200 and resp_code == 0: success += 1 else: fail += 1 costs.append(cost) print(f"成功率: {success}/{success+fail}, " f"平均耗时: {sum(costs)/len(costs)*1000:.1f}ms")
学完这篇,你应该能:
✅ 分清 data= / json= / files= 三种 POST 方式的适用场景✅ 手写 MD5 / SHA256 / HMAC-SHA256 签名函数✅ 理解时间戳 + nonce 防重放的原理并正确实现✅ 完成带签名的 multipart 文件上传完整流程✅ 用 debug 技巧快速定位签名不匹配问题✅ 通过面试中的 POST 相关高频问题
🪤 作业预告:找你们公司或熟悉的开放平台接口文档,用今天的签名流程把文件上传接口跑通。遇到签名不匹配的问题,把签名原文打出来和平台工具对比——这是最快的定位方法。
下期讲 接口自动化测试框架搭建——从单接口测试过渡到 pytest + 数据驱动 + 报告生成的完整工程化实践。
测小狮实战 · A方向 Day 66 · 原创内容,转载需授权
💡 测小狮有话说:GET 是接口自动化的起点,POST 才是真正考验的开始——JSON 序列化、签名算法、文件二进制流,每个都是坎。但搞定了这四个场景(JSON/表单/签名/文件上传),你离独立负责整个模块的自动化测试只差一步。需要本文完整签名上传 Demo 源码 + 六大平台签名对照表的同学,关注我并在后台回复 【666】 即可领取。