news 2026/10/2 6:55:36

网络爬虫与 CDP 实战(二):浏览器能访问,Python 却被拒绝?拆开登录态与 CSRF

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网络爬虫与 CDP 实战(二):浏览器能访问,Python 却被拒绝?拆开登录态与 CSRF

网络爬虫与 CDP 实战(二):浏览器能访问,Python 却被拒绝?拆开登录态与 CSRF

配套实验网站:实验 05:Session 账户 · 实验 06:CSRF 购物车 · 实验 07:Token 账户

上一篇,我们已经找到了真正提供商品数据的接口,也知道如何处理分页和 JSON 请求体。可把 URL 放进 Python 后,接口返回 401;换成 POST,又可能遇到 403。与此同时,浏览器里的页面仍然一切正常。

面对这种现象,继续修改商品选择器解决不了问题。数据源可能已经找对了,差别在于:浏览器发出去的请求,带了 Python 没有携带的状态。

这一篇用三个连续实验拆开这些状态:Cookie 与 Session 让服务端认出用户,CSRF 保护检查某些请求的提交方式,Token 则提供另一种显式凭证链路。随后再延伸到会话复用、存储隔离和接口诊断,看看怎样把一次登录变成可维护的采集流程。

跟着文章动手:在线实验站

本篇的登录、购物车和 Token 实验,都可以在我搭建的网页数据实验场上直接操作。打开首页的 Phase 2,进入实验 05—07;先在浏览器里完成一次业务操作,再用本文代码复现请求。演示账号是demo / demo123,页面与接口已经在线,不需要自己部署登录服务。

网站使用固定的 24 件虚拟商品,页面负责提供业务操作,操作步骤和分析方法在本文中。下面的请求代码默认使用这个公开站点;如有自己的本地实例,可以设置CDP_BASE_URL切换地址。

1. 先区分三种“被拒绝”

不要把所有失败都归结成“反爬”。先回答:请求有没有到达服务器,服务器有没有给出 HTTP 响应,响应内容又属于哪一类?

现象已经能知道什么还需要查什么
网络连接或读取超时这次没有取得完整响应网络路径、服务状态、超时阶段
HTTP 401服务端要求身份信息,或未接受现有凭证Cookie、Token、凭证是否过期
HTTP 403服务端拒绝执行这次请求响应说明、权限、CSRF 等具体规则
HTTP 200,但内容是登录页请求返回了页面,业务结果仍未取得重定向、响应类型、页面内容

401 和 403 的通用语义提供排查入口,却不替你解释某个站点的内部规则。特别是“200 加登录页”,raise_for_status()不会把它当 HTTP 错误;还要检查 Content-Type 与预期数据字段。[1]

打开 Network,找一条浏览器成功请求作为基准。比较方法、路径、参数、Cookie 和 Authorization,先找到差异,再做对照。Cookie 值没有必要在日志中完整打印;是否存在、哪个域和路径下生效、使用后是否被服务端认可,通常已经足够诊断。

2. 实验五:HTTP 不记得你,Session 替服务端保存状态

动手入口:打开Session 账户实验页。先在未登录状态查看账户,再用demo / demo123登录,最后登出并重新请求。把这三个结果与 Cookie 的变化一起记录下来。

设想接口/profile/在登录前返回 401,登录后返回用户名和访问次数。为什么 URL 一样,结果却变了?

HTTP 本身没有“上一条请求已登录”的记忆。服务端可以在登录成功后创建一条 Session(会话)记录,把用户 ID 等信息留在服务端,再返回一个sessionid。浏览器通过响应头Set-Cookie保存它;后续符合 Cookie 匹配规则的请求,会带上Cookie: sessionid=...。服务端凭这个值查回会话状态。[2]

POST 登录 ──> 校验账号 ──> 建立服务端 Session │ Set-Cookie: sessionid=... ↓ 浏览器保存 Cookie ──> GET profile,带 Cookie ──> 服务端查 Session

这里有两个不同位置:Cookie 存在客户端,Session 数据存在哪里取决于服务端实现。示例采用服务端会话记录;别因此推断所有框架都使用同一种存储。有些系统会使用带签名的客户端会话 Cookie,后续的撤销行为也会不同。

用一个客户端走完整生命周期

下列 URL 是配套实验站的接口约定。演示账户为demo / demo123;公开站点的 Session、Token 与门户 ticket 均在签发一小时后失效,登出可以提前撤销。代码展示如何保持会话:

importosimporthttpx BASE=os.environ.get("CDP_BASE_URL","https://web-data-lab.pages.dev").rstrip("/")withhttpx.Client(base_url=BASE,timeout=10.0,trust_env=False)asclient:before=client.get("/labs/session/profile/")ifbefore.status_code!=401:raiseValueError("示例的未登录起点不符合预期")login=client.post("/labs/session/login/",json={"username":"demo","password":"demo123",})login.raise_for_status()print("客户端是否持有 sessionid:",bool(client.cookies.get("sessionid")))for_inrange(2):profile=client.get("/labs/session/profile/")profile.raise_for_status()print(profile.json())client.post("/labs/session/logout/").raise_for_status()after=client.get("/labs/session/profile/")ifafter.status_code!=401:raiseValueError("登出后受限接口仍可访问,需要核对会话规则")

httpx.Client维护跨请求 Cookie,并复用连接。若每次都调用彼此独立的顶层请求,就不能指望上一次登录得到的 Cookie 自动进入下一次调用。[3]

这个实验至少要看到四段证据:登录前拒绝,登录响应下发凭证,后续请求带回凭证,登出后服务端不再接受旧会话。只看客户端“有一个 Cookie”并不能证明登录成功。Cookie 可能已经过期、只在其他路径生效,或者服务器已经撤销对应记录。

3. Cookie 的寿命与作用范围,常比值本身更重要

Cookie 是否发送,涉及域、路径、Secure、SameSite 等属性。HttpOnly限制页面 JavaScript 读取 Cookie,并不阻止浏览器把它带到符合条件的请求里。Secure与加密连接相关;Max-Age、Expires等控制保存时间。[2]

“关闭浏览器后会不会掉登录”也没有统一答案。会话 Cookie、持久 Cookie、浏览器的会话恢复功能,以及服务端 Session 的寿命都会影响结果。真正能确认的是:重新打开页面后的那次请求带了什么,服务端是否接受。

一个容易误判的本地开发问题:端口不同,不代表 Cookie 隔离

假设你在127.0.0.1:8100与127.0.0.1:8200上运行两个项目。Local Storage 按 origin(协议、主机、端口)区分;Cookie 的匹配却不以端口作为隔离维度。因此,同一主机下相同名称、域和路径的 Cookie 可能互相覆盖。仅换端口,不能保证两个项目的 Session Cookie 隔离。[4][9]

这是从教程延伸到实际调试时非常有用的区别。如果两个站点来回切换就掉登录,不妨检查 Cookie 名称与作用范围,而不是先改账号校验代码。使用不同 Cookie 名称、不同主机名或独立浏览器上下文,通常更容易把实验状态分开。

同时也别把 Cookie 罐理解成简单字典:同名 Cookie 可以处在不同域和路径下。跨站复用时,要保留匹配信息,而不是只拼一个裸字符串。

4. 实验六:Cookie 自动带回,为什么 POST 还要 CSRF 令牌

动手入口:打开CSRF 购物车实验页。选择商品和数量,分别尝试页面提供的两种提交操作,观察请求头、Cookie 和返回状态。下面的 Python 示例直接请求这个页面取得令牌,再提交购物车。

现在商品页允许“加入购物车”。你已经有 Session Cookie,但 POST 仍被拒绝。服务端可能在检查 CSRF。

CSRF 是 Cross-Site Request Forgery,跨站请求伪造。问题来自浏览器自动携带凭证的能力:某些跨站请求可能带着用户已有的 Cookie,被目标服务误认为是用户主动发起的操作。具体发送行为还受 SameSite 等规则影响,但它不能简单等同于“浏览器能跨站发请求就一定能读到响应”。[2][5]

CSRF 令牌提供额外检查。合法页面取得令牌,再在表单字段或请求头里主动提交。攻击页面通常不能像同源页面那样读取所需状态;服务端还可能检查 Origin 或 Referer。浏览器自动携带的 Cookie,与页面主动提供的令牌,是两个不同证据。[5]

把令牌的两段路接起来

公开实验站保留了 Django 风格的 CSRF 令牌流程:GET 页面后客户端得到csrftoken。POST 时把它放进X-CSRFToken,同时保持同一个客户端的 Cookie。HTTPS 请求还会检查来源,Python 示例需要发送同源 Referer:

importosimporthttpxwithhttpx.Client(base_url=os.environ.get("CDP_BASE_URL","https://web-data-lab.pages.dev"),timeout=10.0,trust_env=False)asclient:page=client.get("/labs/csrf/")page.raise_for_status()token=client.cookies.get("csrftoken")ifnottoken:raiseValueError("页面没有下发示例所需的 CSRF Cookie")payload={"product_id":1,"quantity":2}success=client.post("/labs/csrf/cart/add/",json=payload,headers={"X-CSRFToken":token,"Referer":str(client.base_url).rstrip("/")+"/labs/csrf/"})success.raise_for_status()missing=client.post("/labs/csrf/cart/add/",json=payload)print("带令牌:",success.status_code,"缺令牌:",missing.status_code)

公开站点的 CSRF 校验由 Worker 实现,并支持 32 字符 Cookie secret 与 64 字符 masked 表单令牌。下面讨论 Django 的配置差异作为扩展背景;不要把所有框架都当成这份站点的实现。

这不是所有 Django 配置通用的取令牌方式。若服务端把 CSRF secret 放在 Session 中,或限制 JavaScript 读取 CSRF Cookie,就需要从页面中的令牌字段取得值。Django 官方文档分别解释了这些配置,并接受表单与 AJAX 头的合法提交路径。[6]

表单字段csrfmiddlewaretoken与 Cookie 中的csrftoken也不一定是逐字符相同的字符串。Django 的表单令牌可以使用随机 mask,服务端验证底层 secret;登录还会轮换 CSRF secret。因此,把旧页面上的令牌保存下来,登录后继续用,可能失败。[5]

对照要一次只改一个条件

先用 Cookie 与令牌齐全的请求建立基线,再只删除请求头,再只篡改令牌,最后换一个没有 Cookie 的客户端。这样才知道失败由哪个条件引起。如果同时改 URL、body 和三个请求头,返回 403 后并没有得到更多理解。

同样,CSRF 令牌不自动代表用户身份。一个未登录页面也可以得到 CSRF 令牌;它通过校验,并不表示具备查询私有库存的权限。身份、权限与请求来源校验,在服务端是不同判断。

5. 实验七:Token 在哪里保存,与谁负责发送,是两个问题

动手入口:打开Token 账户实验页。登录后获取商品,再登出并重复请求。在 Network 中核对 Authorization 请求头,观察它与上一节 Cookie 提交方式的差别。

另一个商品页登录后没有 Session Cookie。响应只返回一个 Token,页面把它放进localStorage,再主动设置Authorization: Bearer <token>。这条链路就与 Session Cookie 不同。

登录响应给 Token ──> 页面脚本存入 localStorage │ 发请求时读取 ↓ Authorization: Bearer <token>

Local Storage 是浏览器按 origin 保存的键值存储,同源脚本可以访问它。它不会自动变成 Authorization 请求头。清掉 Local Storage,只是删除这个浏览器的保存副本;如果另一段代码仍持有有效 Token,服务端可能继续接受它。[4]

importosimporthttpxwithhttpx.Client(base_url=os.environ.get("CDP_BASE_URL","https://web-data-lab.pages.dev"),timeout=10.0,trust_env=False)asclient:login=client.post("/labs/token/login/",json={"username":"demo","password":"demo123",})login.raise_for_status()token=login.json()["token"]headers={"Authorization":f"Bearer{token}"}products=client.get("/labs/token/products/",headers=headers)products.raise_for_status()print(products.json()["count"])client.post("/labs/token/logout/",headers=headers).raise_for_status()revoked=client.get("/labs/token/products/",headers=headers)print("撤销后:",revoked.status_code)

案例中的 Token 是服务端记录的随机令牌,登出删除记录后失效;公开演示站还设置一小时有效期。别把这个结果推广到所有 Token。例如自包含的令牌可以由服务端验签、检查过期时间,不一定每次查询令牌记录;要做到即时撤销,通常还需要额外机制。令牌存储位置,不能替你推断它的服务端状态模型。

另一个常见混淆是:显式 Token 并不会让所有安全问题消失。存储可被页面脚本读取,脚本注入风险仍需考虑;若一个系统同时接受 Cookie 身份,也要分析它实际使用哪条认证路径。对采集程序而言,最有用的结论是准确记录凭证来源、发送位置、有效期与撤销方式。

6. CORS 为什么常常被误当作身份规则

假设页面 Console 报跨域错误,而 Python 能调用同一 API。这并不说明 Python 获得了某种额外业务权限。CORS 是浏览器对跨源资源访问的机制;服务端的身份认证和权限检查仍然可以独立存在。[7]

反过来,Python 返回 401 也不应先去增加 CORS 请求头。先核对 Cookie 或 Token。Origin、Referer是否属于服务端的必要校验,要用成功请求与实际规则确认。手工照抄浏览器的所有头,容易暂时掩盖缺失的凭证流程。

可以把请求拆成三层来记:HTTP 结构是否正确;身份凭证是否被认可;业务或请求校验是否通过。这样面对不同错误,才不会把工具层、浏览器策略与服务端规则揉在一起。

7. 延伸案例:登录复用怎样变成一条可维护的流水线

设想团队要定期同步一个有登录权限的供应商目录。与其每天人工复制 Cookie,不如让流程明确区分四个状态:未登录、已认证、凭证失效、重新认证中。这是教学架构案例,实际登录步骤由系统提供的合法认证流程决定。

认证模块负责取得状态,采集模块负责业务请求,结果模块负责数量与字段验证。业务请求若得到 401,先确认是不是凭证失效;允许重新认证时只做有限次数恢复。如果错误是业务权限不足,不应该无休止登录重试。

若登录过程需要浏览器,可以考虑用 Playwright 的浏览器上下文保存和复用认证状态。其storage_state支持保存相关浏览器状态,但不会自动囊括所有内存变量或 sessionStorage;后者需要单独处理。保存状态只是复用某一时刻的凭证,不保证服务端永远接受它。[8]

复用时还要注意 Cookie 的域路径、Token 的请求头位置,以及多账号是否被意外放在同一个客户端里。客户端只负责带回状态;哪个账号可以访问哪些数据,最终还是服务端决定。认证状态文件本身包含凭证,应按账号配置而不是普通教程附件管理。[8]

一张足够实用的诊断表

浏览器成功、脚本失败时的证据优先怀疑什么下一次对照怎么做
脚本的 Cookie 罐为空登录状态未传递用同一个 Client 完成登录与查询
Cookie 存在,但未出现在业务请求域、路径、Secure 等匹配条件检查实际发出的请求,而不是只看存储
POST 缺X-CSRFToken,浏览器有缺少主动令牌提交补齐取令牌流程,保留 Cookie
登录后旧 CSRF 值失效secret 已轮换登录之后重新读取有效令牌
浏览器带 Authorization,脚本没有Token 只被保存,未被发送按接口要求显式构造请求头
响应变成 HTML 登录页状态失效或重定向核对最终 URL 与 Content-Type

这三个实验最后要留下的不是三个凭证字符串,而是完整状态链:谁签发、在哪里存、谁发送、谁验证、什么时候失效。

下一篇继续推进一个新问题:身份已经正确,Network 响应却没有页面里的目标字段。我们会正式写 CDP 代码,进入浏览器事件、JavaScript 内存、动态 DOM 与请求拦截。

References

[1] HTTP response status codes. MDN Web Docs. Published/updated: not listed. Accessed: 2026-10-01. Used for: HTTP 状态码语义。

[2] Using HTTP cookies. MDN Web Docs. Published/updated: not listed. Accessed: 2026-10-01. Used for: Cookie 往返、属性与作用范围。

[3] Clients. HTTPX. Published/updated: not listed. Accessed: 2026-10-01. Used for: 跨请求 Cookie 与客户端状态。

[4] Window: localStorage property. MDN Web Docs. Published/updated: not listed. Accessed: 2026-10-01. Used for: origin 与浏览器存储。

[5] Cross Site Request Forgery protection. Django documentation. Published/updated: not listed. Accessed: 2026-10-01. Used for: CSRF 校验、mask 与登录轮换。

[6] How to use Django’s CSRF protection. Django documentation. Published/updated: not listed. Accessed: 2026-10-01. Used for: 获取令牌与 AJAX 提交方式。

[7] Cross-Origin Resource Sharing (CORS). MDN Web Docs. Published/updated: not listed. Accessed: 2026-10-01. Used for: 浏览器跨源访问机制。

[8] Authentication. Playwright Python. Published/updated: not listed. Accessed: 2026-10-01. Used for: 认证状态复用与存储边界。

[9] RFC 6265: HTTP State Management Mechanism. IETF / RFC Editor. Published: 2011-04. Accessed: 2026-10-01. Used for: Cookie 不按端口隔离的边界。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 6:55:12

服务器取证实战指南:易失数据采集与磁盘镜像分析要点

1. 进场先别急着碰服务器&#xff1a;定边界、留授权、记现场接到求助电话的时候&#xff0c;通常已经是攻击发生之后了。有人在凌晨两三点打过来&#xff0c;说数据库被拖了&#xff0c;网站被人挂马了&#xff0c;或者干脆就是一句“我们服务器好像被黑了&#xff0c;你快来看…

作者头像 李华
网站建设 2026/10/2 6:54:32

二手苹果设备入库核验:描述文件、联网激活与解除记录怎么查

二手苹果设备的成色和功能检查通过后&#xff0c;还需要核验设备管理状态。对于退租回收、企业批量流转的设备&#xff0c;合同结清、本地描述文件移除和组织侧解除关联&#xff0c;是不同的检查事项。 本文将收货核验整理为三个步骤&#xff1a;检查本地管理信息、抹掉后联网…

作者头像 李华
网站建设 2026/10/2 6:54:26

算法-topK

完整算法&#xff0c;C语言版本&#xff1a;#include <stdio.h> #include <string.h>#define K 10 #define NAME_LEN 50// 一条热搜 typedef struct {char name[NAME_LEN];int hot; // 热度 } HotItem;// // 交换两个热搜 // void swap(HotItem *…

作者头像 李华
网站建设 2026/10/2 6:53:42

Meta-Muse 是什么,能干嘛?含注册路径

全文约 3400 字 阅读约 8 分钟 零基础可读 看懂 Muse 能干到哪一步&#xff0c;也知道第一件事该怎么放心交给它。 让 AI 帮你规划旅行&#xff0c;它很快写出一份行程。哪天去哪儿&#xff0c;吃什么&#xff0c;玩什么&#xff0c;安排得明明白白。 接下来呢&#xff1f;…

作者头像 李华
网站建设 2026/10/2 6:53:20

mac系统GSEA R包安装问题求助

clusterProfiler org.Hs.eg.db enrichplot这三个包安装失败&#xff0c;R版本4.4.3bioconductor手动下载会一直下载依赖包求助&#x1f62d;

作者头像 李华