bigywater.tech
爬虫工程 · 51

表单与 CSRF token:登录流程的完整请求序列

登录一个有 CSRF 保护的表单,请求序列比想象的长:先取页面拿 cookie 和 token,再带着所有隐藏字段 POST,失败后 token 还会换。这篇在本机写一个登录页,逐步走通正确的流程,再逐个故意做错,看服务器怎么拒绝。

403 / 400
漏掉 token 或 cookie 是 403;缺隐藏字段或多填了不该填的是 400
2 个 cookie
登录成功前后会话 cookie 的值不同——服务器在登录时换了新的会话 id
旧 token → 403
登录失败后 token 轮换,用旧 token 再提交正确密码被拒绝

正确的流程

实测 · 本机执行输出
  ① GET /login → 200,Set-Cookie: sid=65e3a71108ced97f; Path=/; …;页面里的表单字段(含隐藏字段)= {'csrf_token': '4a1ed29ca5ebf5a8', 'form_id': 'login-v2', 'next': '/account', 'username': '', 'password': '', 'website': ''}
  ② POST /login(带上全部隐藏字段和账号密码)→ 302,Location=/account,Set-Cookie: sid=90a57a610d82c7af; Path=/; …(登录成功后会话 id 换了)
  ③ GET /account → 200 'welcome alice'
  会话 cookie 在登录前后的值:sid=65e3a71108ced97f → sid=90a57a610d82c7af(不同)

先 GET /login 拿到会话 cookie 和页面里的 csrf_token,再 POST /login 带上表单里所有字段(隐藏字段取默认值,只填账号密码),成功后 302 跳转并换新的会话 id。requests.Session 负责 cookie 的往返。

常见的错误

实测 · 本机执行输出
  没先 GET 表单、没有 cookie 就直接 POST:403 'no session'
  有 cookie 但漏了 csrf_token 字段:403 'csrf token mismatch'
  csrf_token 值不对:403 'csrf token mismatch'
  token 对但漏了另一个隐藏字段 form_id:400 'missing form_id'
  把页面上所有 text 输入框都填了(包括 CSS 隐藏的蜜罐 website):400 'honeypot filled'
  用会话 A 的 token 配会话 B 的 cookie:403 'csrf token mismatch'

不先取表单、漏 token、token 不对、拿别的会话的 token,都是 403;缺别的隐藏字段是 400;把页面上所有输入框都填了(包括 CSS 隐藏的)会被服务器拒绝。做法是像浏览器一样提交:收集所有带 name 的 input,隐藏字段原样带回,只覆盖账号密码。

token 轮换

实测 · 本机执行输出
  ① 密码错误:401(服务器换了新的 token)
  ② 还用旧 token 再提交正确密码:403 'csrf token mismatch'
  ③ 重新 GET 表单拿新 token 后提交:302

登录失败后服务器换了新 token,旧 token 再提交被拒绝。重试要重新 GET 表单。

AJAX 与 meta

实测 · 本机执行输出
  只发 JSON、不带头:403 'csrf header mismatch'
  带 X-CSRF-Token: 200 '{"ok": true}'

前端发 JSON 请求时,token 常放在 <meta name="csrf-token"> 里,用 X-CSRF-Token 请求头回传;只发 JSON 不带头会被拒绝。

收集表单字段

实测 · 本机执行输出
  做法:取表单里所有带 name 的 <input>(隐藏字段取默认值),只覆盖用户名、密码;不要碰蜜罐和其它隐藏字段。上面第一节就是这样做的:字段 = ['csrf_token', 'form_id', 'next', 'password', 'username', 'website']

在这个页面上表单共有 6 个字段,爬虫只覆盖 username 和 password,其余按页面原样提交。

分步演示

表单与 CSRF
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。本机服务器自己实现的登录页:会话 cookie(sid)、与会话绑定的 csrf_token(失败后轮换)、隐藏字段 form_id 与 next、CSS 隐藏的 website 输入框、登录成功时更换会话 id、/api/profile 要求 X-CSRF-Token 头;lxml 6.1.1 解析表单;全部在本机完成,不涉及任何真实网站的登录
    • 没有验证:真实站点的登录(验证码、短信、设备指纹、速率限制、多步验证);SameSite、Secure 等 cookie 属性的影响;CSRF token 绑定到 cookie 本身(双重提交 cookie)的实现;JavaScript 在提交前动态生成的字段或签名;OAuth/SSO 登录流程;会话固定与会话劫持的防护细节
    京ICP备2026067502号-1京公网安备11010502063487号