ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/10/2 6:55:39 拓冰建站 浏览量
网络爬虫与 CDP 实战(二):浏览器能访问,Python 却被拒绝?拆开登录态与 CSRF 网络爬虫与 CDP 实战二浏览器能访问Python 却被拒绝拆开登录态与 CSRF配套实验网站实验 05Session 账户 · 实验 06CSRF 购物车 · 实验 07Token 账户上一篇我们已经找到了真正提供商品数据的接口也知道如何处理分页和 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 BASEos.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev).rstrip(/)withhttpx.Client(base_urlBASE,timeout10.0,trust_envFalse)asclient:beforeclient.get(/labs/session/profile/)ifbefore.status_code!401:raiseValueError(示例的未登录起点不符合预期)loginclient.post(/labs/session/login/,json{username:demo,password:demo123,})login.raise_for_status()print(客户端是否持有 sessionid,bool(client.cookies.get(sessionid)))for_inrange(2):profileclient.get(/labs/session/profile/)profile.raise_for_status()print(profile.json())client.post(/labs/session/logout/).raise_for_status()afterclient.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 示例需要发送同源 Refererimportosimporthttpxwithhttpx.Client(base_urlos.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev),timeout10.0,trust_envFalse)asclient:pageclient.get(/labs/csrf/)page.raise_for_status()tokenclient.cookies.get(csrftoken)ifnottoken:raiseValueError(页面没有下发示例所需的 CSRF Cookie)payload{product_id:1,quantity:2}successclient.post(/labs/csrf/cart/add/,jsonpayload,headers{X-CSRFToken:token,Referer:str(client.base_url).rstrip(/)/labs/csrf/})success.raise_for_status()missingclient.post(/labs/csrf/cart/add/,jsonpayload)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 tokenLocal Storage 是浏览器按 origin 保存的键值存储同源脚本可以访问它。它不会自动变成 Authorization 请求头。清掉 Local Storage只是删除这个浏览器的保存副本如果另一段代码仍持有有效 Token服务端可能继续接受它。[4]importosimporthttpxwithhttpx.Client(base_urlos.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev),timeout10.0,trust_envFalse)asclient:loginclient.post(/labs/token/login/,json{username:demo,password:demo123,})login.raise_for_status()tokenlogin.json()[token]headers{Authorization:fBearer{token}}productsclient.get(/labs/token/products/,headersheaders)products.raise_for_status()print(products.json()[count])client.post(/labs/token/logout/,headersheaders).raise_for_status()revokedclient.get(/labs/token/products/,headersheaders)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 不按端口隔离的边界。