☰
Python requests 请求 Django 站点 CSRF 校验失败解决指南
2026/10/16 9:47:40 网站建设 项目流程

1. 从一个真实场景说起:为什么你的请求总是被拒

用 Python 的 requests 库去请求一个 Django 搭建的网站,代码写得好好的,URL 没错、参数也对,可返回的状态码偏偏是 403。打开返回内容一看,提示 CSRF verification failed。这个场景我见过太多次了,不管是做接口自动化测试、写爬虫采集公开数据,还是对接内部系统的 API,只要目标站点用了 Django 默认的 CSRF 防护机制,裸奔的 POST 请求基本都会被拦下来。

这个问题的核心不在于 requests 不好用,而在于 Django 的 CSRF 防护是一套完整的“令牌校验”机制,它要求客户端在提交表单时,必须携带一个由服务端签发、且与当前会话绑定的令牌。requests 默认不会自动处理这套机制,所以你得手动把整个流程串起来。

这篇文章要解决的就是这个问题:如何用 requests 正确地请求带有 CSRF token 校验的 Django 站点。我会从 CSRF 的防护原理讲起,然后一步步拆解完整的请求流程,包括怎么拿 token、怎么带 token、怎么维持会话、怎么处理 token 过期,最后把我踩过的坑和排查技巧都整理出来。适合有一定 Python 基础、正在做接口测试或数据采集的开发者参考,新手也能跟着一步步操作。

2. 先搞懂 Django 的 CSRF 防护到底在防什么

2.1 CSRF 攻击的基本原理

CSRF 全称是 Cross-Site Request Forgery,跨站请求伪造。攻击的逻辑是这样的:你登录了某个网站 A,浏览器里存着 A 的会话 Cookie。这时候你手贱点开了另一个恶意网站 B,B 的页面里藏了一个自动提交的表单,目标地址指向 A 的某个敏感操作接口,比如转账、改密码。因为你浏览器里带着 A 的 Cookie,这个请求发出去之后,A 的服务端会认为是你本人操作的,于是攻击就成功了。

这个攻击的关键在于:浏览器会自动携带目标站点的 Cookie,而服务端光凭 Cookie 无法区分这个请求是你主动发的还是被恶意页面诱导发的。

2.2 Django 的令牌校验机制

Django 的应对方案是引入一个额外的令牌。服务端在渲染表单页面时,会生成一个随机的 CSRF token,同时把它写到页面的隐藏字段里,另外还会在 Cookie 里种一个名为 csrftoken 的值。当你提交表单时,Django 会同时检查两样东西:请求体里的 token 和 Cookie 里的 token,两者必须匹配,而且还要通过一套密钥验证。

这就意味着,攻击者的恶意页面虽然能让浏览器自动带上 Cookie,但它拿不到页面里的那个隐藏 token 值,所以请求就会被拒绝。这套机制的精妙之处在于,token 是嵌入在页面内容里的,而恶意站点受同源策略限制,读不到目标站点的页面内容。

2.3 为什么 requests 默认过不了这一关

requests 是一个通用的 HTTP 客户端,它不会去解析 HTML 页面、提取隐藏字段、维护 Cookie 和 token 的对应关系。你发一个 POST 请求,它就老老实实把你能给的数据发出去,没有 token 就是没有 token,Django 自然不认。

所以我们要做的事情,本质上就是模拟浏览器的行为:先 GET 一次页面拿到 token,把 Cookie 存下来,然后在 POST 的时候把 token 带上,并且保证 Cookie 和 token 是配套的。

3. 完整请求流程的设计思路

3.1 核心思路:先取令牌,再带令牌提交

整个流程可以拆成三步。第一步,用 GET 请求访问包含表单的页面,从响应中提取 CSRF token,同时让 requests 自动保存服务端下发的 Cookie。第二步,从 Cookie 中读取 csrftoken 的值,或者直接复用页面里提取的 token。第三步,构造 POST 请求,在请求头或表单数据中带上 token,同时携带之前保存的 Cookie。

这里有个关键点:token 和 Cookie 必须是同一次会话产生的。如果你用 A 会话拿的 token 配 B 会话的 Cookie,校验照样失败。所以维持会话的一致性非常重要。

3.2 为什么推荐用 Session 对象而不是裸 requests

很多人习惯直接用 requests.get() 和 requests.post(),这样每次请求都是独立的,Cookie 不会自动传递。正确的做法是创建一个 requests.Session() 对象,用它来发所有请求。Session 会自动管理 Cookie,第一次 GET 拿到的 csrftoken 会被存下来,后续 POST 时自动带上,省去了手动处理 Cookie 的麻烦。

我实测下来,用 Session 能让代码简洁一大半,而且不容易出错。裸 requests 需要你手动从 response.cookies 里取值再塞到下次请求的 cookies 参数里,稍不注意就漏了。

3.3 token 放在哪里:请求头还是表单数据

Django 对 token 的接收位置是有讲究的。传统的表单提交,token 放在 POST 数据的 csrfmiddlewaretoken 字段里。而 AJAX 请求,Django 默认期望 token 放在请求头 X-CSRFToken 里。这两种方式 Django 都支持,具体用哪种取决于目标接口的设计。

我的经验是:如果目标是一个传统的表单页面,用表单字段的方式更稳妥;如果是前后端分离的接口,优先用请求头。拿不准的时候,两种都带上也不会有问题,Django 会按优先级去取。

4. 手把手实现:从取 token 到成功提交

4.1 第一步:创建 Session 并获取初始页面

先看代码,这是整个流程的起点:

import requests from bs4 import BeautifulSoup session = requests.Session() # 设置一个常见的浏览器 UA,避免被简单的 UA 过滤拦截 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" } session.headers.update(headers) # 访问包含表单的页面 login_url = "https://example-site.com/login/" response = session.get(login_url) print(response.status_code)

这一步做完之后,session 对象里已经存了服务端下发的 csrftoken Cookie。你可以打印 session.cookies 看一下,通常能看到一个名为 csrftoken 的条目。

注意:有些站点会把 token 藏在 JavaScript 变量里而不是隐藏字段里,这种情况用 BeautifulSoup 解析 HTML 是拿不到的,需要额外用正则去匹配 script 标签里的内容。

4.2 第二步:从页面中提取 CSRF token

拿到页面 HTML 之后,用 BeautifulSoup 定位隐藏字段:

soup = BeautifulSoup(response.text, "html.parser") # Django 表单里的隐藏字段名固定是 csrfmiddlewaretoken token_input = soup.find("input", {"name": "csrfmiddlewaretoken"}) if token_input: csrf_token = token_input.get("value") print("提取到的 token:", csrf_token) else: print("页面里没有找到 csrfmiddlewaretoken 字段")

如果页面里确实没有这个字段,别急着放弃。先去 session.cookies 里看看有没有 csrftoken,有的话可以直接用那个值。Django 的校验逻辑是:请求体里的 token 和 Cookie 里的 token 经过同样的密钥处理后应该一致,而页面里的 csrfmiddlewaretoken 本身就是从 Cookie 的 csrftoken 派生出来的,所以直接用 Cookie 里的值往往也能通过。

4.3 第三步:构造带 token 的 POST 请求

现在把 token 带上,提交表单:

post_url = "https://example-site.com/login/" # 方式一:token 放在表单数据里 form_data = { "username": "your_username", "password": "your_password", "csrfmiddlewaretoken": csrf_token, } # 方式二:token 放在请求头里(AJAX 风格) post_headers = { "X-CSRFToken": csrf_token, "Referer": login_url, } response = session.post(post_url, data=form_data, headers=post_headers) print(response.status_code) print(response.text[:500])

这里我同时用了两种方式,实际项目中选一种即可。另外注意 Referer 头,Django 在 HTTPS 环境下会校验 Referer 是否同源,如果缺失或者不对,也会触发 403。这个坑我踩过,当时排查了半天才发现是 Referer 的问题。

4.4 完整代码整合

把上面的步骤串起来,形成一个可以直接运行的脚本:

import requests from bs4 import BeautifulSoup def login_with_csrf(login_url, post_url, username, password): session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" }) # 第一步:GET 页面,拿 Cookie 和 token resp = session.get(login_url) soup = BeautifulSoup(resp.text, "html.parser") token_input = soup.find("input", {"name": "csrfmiddlewaretoken"}) if token_input: csrf_token = token_input.get("value") else: csrf_token = session.cookies.get("csrftoken") if not csrf_token: raise RuntimeError("未能获取到 CSRF token,检查页面结构或 Cookie") # 第二步:POST 提交 form_data = { "username": username, "password": password, "csrfmiddlewaretoken": csrf_token, } headers = { "X-CSRFToken": csrf_token, "Referer": login_url, } resp = session.post(post_url, data=form_data, headers=headers) return resp if __name__ == "__main__": result = login_with_csrf( "https://example-site.com/login/", "https://example-site.com/login/", "your_username", "your_password" ) print(result.status_code)

这段代码覆盖了大部分 Django 站点的登录场景。你可以根据目标站点的实际字段名做调整。

5. 参数细节与常见坑点解析

5.1 Cookie 与 token 的绑定关系

Django 的 CSRF 校验有一个容易被忽略的细节:它会把 Cookie 里的 csrftoken 和请求里的 token 做一次“加盐”处理,然后比对结果。这个加盐用的密钥是服务端随机生成的,每次请求可能不同。所以你不能自己伪造一个 token,必须用服务端下发的。

另外,如果你在同一个 Session 里多次 GET 页面,csrftoken 可能会被刷新。这时候旧的 token 就失效了,必须用最新的。我遇到过一种情况:先 GET 了页面 A 拿到 token,然后又 GET 了页面 B,结果 B 刷新了 Cookie,导致 A 的 token 作废。解决办法是每次 POST 之前重新 GET 一次目标页面,确保 token 是最新的。

5.2 Referer 校验的触发条件

Django 的 CsrfViewMiddleware 在 HTTPS 请求下会检查 Referer 头。如果请求是 HTTPS 的,而 Referer 缺失或者域名不匹配,就会直接拒绝。这个行为由 CSRF_TRUSTED_ORIGINS 配置控制,但作为客户端我们改不了服务端配置,只能老老实实把 Referer 带上。

实测下来,Referer 的值应该是目标站点的完整 URL,包括协议和域名。有些站点还会校验路径,所以最好用表单页面的 URL 作为 Referer。

5.3 token 过期与刷新策略

CSRF token 是有有效期的,默认和会话绑定。如果会话过期了,token 也就失效了。在长时间运行的脚本里,你需要处理 token 过期的情况。我的做法是封装一个重试逻辑:如果 POST 返回 403,就重新 GET 一次页面刷新 token,然后再试一次。

def post_with_retry(session, url, data, headers, max_retries=2): for attempt in range(max_retries): resp = session.post(url, data=data, headers=headers) if resp.status_code != 403: return resp # 403 了,刷新 token 再试 page = session.get(url) soup = BeautifulSoup(page.text, "html.parser") token_input = soup.find("input", {"name": "csrfmiddlewaretoken"}) if token_input: new_token = token_input.get("value") data["csrfmiddlewaretoken"] = new_token headers["X-CSRFToken"] = new_token return resp

这个重试逻辑在实际项目中非常实用,尤其是做批量操作的时候。

5.4 常见问题速查表

问题现象可能原因解决方向
返回 403,提示 CSRF verification failed请求里没有 token检查是否提取并携带了 token
返回 403,提示 CSRF cookie not setCookie 里没有 csrftoken确认先 GET 了页面,且 Session 正常保存 Cookie
返回 403,提示 Referer checking failedReferer 缺失或不对添加正确的 Referer 头
token 提取为空页面结构变了或 token 在 JS 里检查页面源码,改用正则匹配
第一次成功,第二次失败token 被刷新了每次请求前重新获取 token
本地测试通过,线上失败环境差异导致 Cookie 策略不同检查域名、协议、端口是否一致

6. 进阶场景:接口测试与批量请求中的实践

6.1 在接口自动化测试中复用 Session

做接口测试的时候,通常需要先登录拿到会话,然后连续调用多个接口。这时候 Session 对象的复用就很重要。我的做法是把登录逻辑封装成一个 fixture,在测试开始前执行一次,后续所有请求都复用这个 Session。

import pytest @pytest.fixture(scope="session") def logged_in_session(): session = requests.Session() # 执行登录流程 # ... return session def test_get_profile(logged_in_session): resp = logged_in_session.get("https://example-site.com/api/profile/") assert resp.status_code == 200

这样写的好处是登录只做一次,测试跑得快,而且不会因为频繁登录触发风控。

6.2 处理前后端分离的接口

现在很多 Django 项目用 DRF 做后端,前端用 Vue 或 React。这种架构下,CSRF token 通常通过 Cookie 下发,前端从 Cookie 里读出来放到请求头里。用 requests 模拟的时候,流程是一样的:先 GET 一个接口让服务端种下 csrftoken,然后从 session.cookies 里读出来,放到 X-CSRFToken 头里。

需要注意的是,有些 DRF 项目会关闭 CSRF 校验(用 @csrf_exempt 装饰器),这时候你反而不用带 token。所以动手之前,先用浏览器开发者工具看一下实际请求带了什么,照着抄就行。

6.3 批量请求时的性能考量

如果需要发起大量请求,每次都 GET 页面拿 token 会很慢。优化思路是:登录之后,token 在会话有效期内是复用的,不需要每次请求都重新获取。只有在收到 403 的时候才去刷新。这样能把请求量降下来。

另外,Session 对象本身是线程不安全的,如果你用多线程并发请求,每个线程应该有自己的 Session。这一点在做压力测试的时候要特别注意。

7. 我踩过的那些坑和排查心得

第一个坑是 UA 的问题。有些站点会对 requests 默认的 User-Agent 做拦截,直接返回 403,但提示信息不是 CSRF 相关的,容易误导排查方向。所以第一步就应该把 UA 设置成常见浏览器。

第二个坑是 Cookie 的作用域。如果你访问的页面和提交的接口不在同一个路径下,Cookie 的 path 属性可能导致 Cookie 不被发送。这种情况需要检查 Set-Cookie 头里的 path 字段,必要时手动调整。

第三个坑是 HTTPS 证书。有些测试环境用的是自签名证书,requests 默认会校验失败。可以用 verify=False 跳过,但生产环境千万别这么干。

排查的时候,我习惯用 curl 或者浏览器开发者工具先手动跑一遍流程,把实际的请求头、Cookie、表单数据都抓下来,然后照着用 requests 复现。这样比盲猜快得多。另外一个技巧是打开 Django 的日志,看看服务端到底是因为哪个环节拒绝的,日志里通常会写明具体原因。

最后分享一个小经验:如果你拿到的 token 里包含换行符或者空格,记得 strip 一下。我有一次就是因为 token 末尾多了个换行,导致校验一直失败,排查了快一个小时才发现。这种细节问题最折磨人,但也最容易避免。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询