Python requests 请求 Django 站点 CSRF 校验失败解决指南
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。这时候你手贱点开了另一个恶意网站 BB 的页面里藏了一个自动提交的表单目标地址指向 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 就是没有 tokenDjango 自然不认。所以我们要做的事情本质上就是模拟浏览器的行为先 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, dataform_data, headerspost_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, dataform_data, headersheaders) 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_retries2): for attempt in range(max_retries): resp session.post(url, datadata, headersheaders) 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(scopesession) 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 默认会校验失败。可以用 verifyFalse 跳过但生产环境千万别这么干。排查的时候我习惯用 curl 或者浏览器开发者工具先手动跑一遍流程把实际的请求头、Cookie、表单数据都抓下来然后照着用 requests 复现。这样比盲猜快得多。另外一个技巧是打开 Django 的日志看看服务端到底是因为哪个环节拒绝的日志里通常会写明具体原因。最后分享一个小经验如果你拿到的 token 里包含换行符或者空格记得 strip 一下。我有一次就是因为 token 末尾多了个换行导致校验一直失败排查了快一个小时才发现。这种细节问题最折磨人但也最容易避免。

相关新闻

SQL注入之报错注入全解析:原理、姿势与防御技巧

SQL注入之报错注入全解析:原理、姿势与防御技巧

有的新手朋友第一次接触 SQL注入时都会忍不住想:既然有 union 注入可以直接回显数据,那报错注入到底还有什么用?说实话,最初我也有同样的疑问。后来在实际测试里碰了几次壁,才真正意识到报错注入的特殊价值——它不需要…

2026/10/16 9:48:06 阅读更多 →
VPP源码解析:插件机制与node graph批处理框架实践

VPP源码解析:插件机制与node graph批处理框架实践

简介:VPP(Vector Packet Processing)是FD.io社区的高性能数据包处理框架,广泛用于网络功能虚拟化、路由与交换场景。面向网络开发工程师、VPP二次开发学习者及SDN/NFV研究者,资料提供VPP完整源码和在其上编写示例插件的…

2026/10/16 9:48:06 阅读更多 →
FastReport Excel模板引擎:用.xlsx做报表设计源文件

FastReport Excel模板引擎:用.xlsx做报表设计源文件

简介:本资源面向Delphi中高级开发者,聚焦Excel与FastReport报表组件的深度集成实践,解决在Delphi项目中快速构建数据驱动型报表的实际难题。资源包共10个文件,含Delphi核心工程文件(.dpr、.dfm、.pas)、可直…

2026/10/16 9:47:04 阅读更多 →

最新新闻

C#面向对象:Person类、Student类继承与构造函数全解析

C#面向对象:Person类、Student类继承与构造函数全解析

1. 面向对象的第一步:从Person类的设计说起很多人学C#敲的第一段面向对象代码,就是从定义人类Person开始的。这个题目看着简单,但背后其实藏着一堆值得掰扯清楚的细节——构造函数怎么写、字段和属性有什么区别、继承之后构造函数怎么联动。我…

2026/10/16 10:41:46 阅读更多 →
Tailwind CSS组件库v6.23:纯CSS驱动的62个可复用UI模块

Tailwind CSS组件库v6.23:纯CSS驱动的62个可复用UI模块

简介:本资源是 Tailwind CSS 6.23 版本的完整组件实践包,面向前端开发者、UI 工程师及响应式项目快速搭建者,解决传统 CSS 开发效率低、定制难、响应式适配繁琐等痛点。压缩包含 2007 个文件,主体为 1304 个 HTML 示例页&#xff…

2026/10/16 10:41:46 阅读更多 →
线性加权滑移平均精准捕捉局部小峰值的Matlab实现与调参

线性加权滑移平均精准捕捉局部小峰值的Matlab实现与调参

前阵子处理一段传感器信号时,我遇到一个特别典型的困境:数据里明明有很关键的局部小峰值,结果一用常规的滑动平均去噪,峰值幅度被削掉一大半,位置还往后偏移了好几个采样点。后来换成线性加权滑移平均算法,…

2026/10/16 10:41:46 阅读更多 →
用Niagara制作逼真火焰:噪声、颜色曲线与透明淡出全解析

用Niagara制作逼真火焰:噪声、颜色曲线与透明淡出全解析

从小我就觉得粒子特效是门“骗术”:喷泉喷的是水,火焰烧的是粒子,但只要观感对了,没人会在意那是不是真的流体。喷泉那一篇发出来后,不少人问我能不能把发射器关掉重力、换个橙色贴图,直接当火焰用。我的回…

2026/10/16 10:41:46 阅读更多 →
Utilman.exe丢失或找不到怎么办?系统自带安全修复方法全解析

Utilman.exe丢失或找不到怎么办?系统自带安全修复方法全解析

系统突然出现“Utilman.exe文件丢失或找不到”的报错,登录界面右下角的“轻松使用”按钮点了没反应,甚至开机就卡在欢迎界面。这个问题在Windows 7到Windows 11上都可能出现,不少朋友第一反应是去第三方下载站找个exe文件补上——我劝你千万打…

2026/10/16 10:41:46 阅读更多 →
AI逆向工程实战:大语言模型如何将二进制翻译为伪代码

AI逆向工程实战:大语言模型如何将二进制翻译为伪代码

这个标题是我在刷开源社区动态的时候看到的,第一反应是“又来了个套壳项目”,但点进去翻了源码和论文之后,我承认我小看它了。这个项目目前在 GitHub 上拿下了 31.2K Star,热度确实不是刷出来的。我花了两天时间把它部署到本地&am…

2026/10/16 10:40:41 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →