不装任何第三方库,用 Python 标准库做一次带 CSRF 的 HTTP 调用
场景
要给一个内部系统做自动化,接口需要 CSRF token。环境不允许装 `requests`,只能用标准库。
一、先看清楚 token 从哪来
打开浏览器开发者工具,看请求头。常见三种来源:
1. **响应头下发**:`GET /api/bootstrap` 的响应头里有 `csrf: xxx`
2. **Cookie**:`csrf-token=xxx`,前端读出来塞进请求头
3. **页面内嵌**:HTML 里 ``
我遇到的是第一种。判断方法:在页面控制台执行 `document.cookie`,如果里面没有 csrf,那大概率是响应头或内存变量。
二、用 urllib 实现
import json
import urllib.request
import urllib.error
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/140.0 Safari/537.36"
def get(url, cookie="", extra=None):
headers = {"User-Agent": UA, "Accept": "application/json"}
if cookie:
headers["Cookie"] = cookie
if extra:
headers.update(extra)
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=20) as r:
return r.status, dict(r.headers), r.read().decode()
except urllib.error.HTTPError as e:
return e.code, dict(e.headers), e.read().decode("utf-8", "replace")
# 第一步:拿 token
status, headers, _ = get("https://example.com/api/bootstrap")
csrf = {k.lower(): v for k, v in headers.items()}.get("csrf")
# 第二步:带上它
status, headers, body = get(
"https://example.com/api/data",
cookie="sid=abc123",
extra={"csrf": csrf},
)
print(status, body[:200])
三、Cookie 拼接有个陷阱
如果直接把所有 cookie 拼成一个字符串,很容易出错。原因:
• 有些 cookie 是 host-only(只对特定域名生效)
• 有些是 domain cookie(对子域生效)
• 同名 cookie 在父子域可能各有一份
正确做法是按目标 host 过滤:
from urllib.parse import urlparse
def jar_for(cookies, url):
host = urlparse(url).hostname
jar = {}
for c in cookies:
dom = c["domain"].lstrip(".")
if c["domain"].startswith("."):
if not (host == dom or host.endswith("." + dom)):
continue
elif host != dom:
continue
prev = jar.get(c["name"])
# 更具体的域优先
if prev is None or len(dom) > len(prev[0]):
jar[c["name"]] = (dom, c["value"])
return jar
我之前就是因为把所有 cookie 无脑拼上,导致请求打到了错误的边缘节点,返回 `404 no Route matched`——排查了很久才发现是 cookie 污染。
四、什么时候该放弃纯 HTTP
如果出现以下情况,说明纯 HTTP 走不通,该上浏览器:
• 补了 token、cookie、完整请求头,仍然 403
• 响应内容明确说「未登录」,但浏览器里同一个账号是登录状态
• 服务器识别到非浏览器指纹(TLS 指纹、`sec-ch-ua` 等)
我遇到的真实案例:同一个 profile,**无头浏览器**返回未登录,**非无头浏览器**正常。这种只能上真实浏览器 + 虚拟显示器。
五、错误处理的几个细节
try:
...
except urllib.error.HTTPError as e:
# 注意:HTTPError 本身也是 response,能读 body
body = e.read().decode("utf-8", "replace")
if "login" in body.lower():
raise AuthExpired("登录态失效")
except urllib.error.URLError as e:
# 网络层错误(DNS、超时、拒绝连接)
raise NetworkError(str(e.reason))
区分「HTTP 层失败」和「网络层失败」很重要,前者要重新登录,后者只需重试。
小结
标准库够用,但要处理好三件事:token 来源、cookie 作用域、错误分类。如果这些都对了还是不通,果断换浏览器,别在 HTTP 层死磕。
- 点赞
- 收藏
- 关注作者
评论(0)