Python 爬虫模拟登录教程:Cookie、JWT 与 Playwright 三种方案
Python 爬虫如何模拟登录?本文从 Network 抓包判断 Cookie、JWT/Token 与浏览器状态,给出 requests.Session、Authorization、Playwright 三种可运行方案,并附 401/403 排查清单。
先判断登录凭证存在哪里
Python 爬虫模拟登录的核心,不是“把账号密码 POST 一遍”,而是拿到网站认可的身份凭证,并在后续请求中按原来的方式携带。打开浏览器开发者工具完成一次正常登录,通常会看到两条主线:服务器通过 Set-Cookie 建立会话,或者登录接口返回 Token,后续请求再通过 Authorization 等请求头携带。
| 浏览器里的现象 | 常见认证方式 | Python 侧的处理 |
|---|---|---|
登录响应有 Set-Cookie,后续请求带 Cookie |
Cookie / 服务端 Session | 用同一个 requests.Session 完成登录和数据请求 |
登录响应 JSON 里有 access_token 等字段 |
Token 认证,Token 可能是 JWT | 读取 Token,按真实请求格式设置 Authorization |
| 登录参数经过脚本计算,或必须完成复杂交互 | 浏览器登录 | 用 Playwright 完成登录,再复用 Cookie 或浏览器状态 |
| Cookie 已有但接口仍要求额外请求头 | Cookie + CSRF Token 等组合 | 同时还原 Cookie、CSRF Token 和必要请求头 |

这里要分清两个容易混用的概念:JWT 是一种 Token 格式,Bearer 是一种携带访问令牌的 HTTP 认证方案。接口字段叫 access_token,不代表它一定是 JWT;JWT 也不一定由 Authorization: Bearer ... 携带。最可靠的依据始终是目标系统的接口文档,以及你在授权环境中观察到的真实请求。
MDN 的 Set-Cookie 文档说明了服务器如何让客户端保存 Cookie;RFC 6750则定义了 Bearer Token 放入 Authorization 请求头的标准写法。
用 Network 还原一次真实登录
先在你有权访问的测试账号或教学靶场里手动登录一次,再写代码。Chrome DevTools 的 Network 面板建议按这个顺序看:
- 勾选 Preserve log,避免页面跳转后登录请求消失。
- 清空旧记录,重新提交一次登录表单。
- 找到提交用户名和密码的
POST请求,确认 Request URL 和 Method。 - 查看 Payload:
Form Data通常对应data=,Request Payload中的 JSON 通常对应json=。 - 查看登录响应:重点找
Set-Cookie、Location、Token 字段和业务成功标志。 - 再点开真正返回数据的 Fetch/XHR 请求,确认它携带的是 Cookie、
Authorization,还是两者之外的 CSRF 请求头。
课程第 64 节的 Cookie 案例里,使用正确凭证登录成功时,响应会写入会话标识 Cookie(sessionid)并返回 303 See Other,浏览器随后访问 Location 指向的新地址。303 常用于 POST 后跳转,并不表示登录失败;客户端会改用 GET 请求新地址。想单独观察这一步,可以在 Requests 登录请求里临时传入 allow_redirects=False。
不要把浏览器里的所有请求头原样复制进代码。Content-Length、Host、大部分 Sec-* 字段通常由客户端自动处理。先保留接口明确依赖的 Content-Type、Referer、CSRF 头和业务自定义头,代码会更稳定,也更容易排查。
方案一:用 requests.Session 保持 Cookie 登录态
普通表单登录最适合从 requests.Session 开始。Session 会在同一个实例中保存响应写入的 Cookie,并在后续符合域名和路径规则的请求里自动带上;它还会复用底层连接。这个行为可以在 Requests Session 官方文档中核对。
import os
import requests
BASE_URL = "https://example.com"
def fetch_private_data(page=1):
# 账号密码从环境变量读取,避免提交到代码仓库
username = os.environ["DEMO_USERNAME"]
password = os.environ["DEMO_PASSWORD"]
with requests.Session() as session:
session.headers.update({
"User-Agent": "Mozilla/5.0",
"Accept": "application/json, text/plain, */*",
})
login_response = session.post(
f"{BASE_URL}/login",
data={
"username": username,
"password": password,
},
timeout=10,
)
login_response.raise_for_status()
# 登录和数据请求必须复用同一个 Session
data_response = session.get(
f"{BASE_URL}/api/private-data",
params={"page": page},
timeout=10,
)
data_response.raise_for_status()
# 有些网站会把未登录请求静默重定向回登录页
if "/login" in data_response.url:
raise RuntimeError("登录态未生效,请检查 Cookie、表单字段和重定向")
return data_response.json()
这段代码有三个不能省的点:
- 登录 POST 和数据 GET 使用同一个
Session,否则刚拿到的 Cookie 会丢失。 - 每个外部请求都设置超时,并用
raise_for_status()处理 HTTP 错误。 - 不把“登录请求返回 200”直接当成成功,而是继续访问受保护接口验证登录态。
如果 Network 里显示登录请求体是 JSON,把 data={...} 改为 json={...}。不要一边传表单,一边手动写 Content-Type: application/json,请求体格式和请求头不一致时,服务端常会返回 400、422,或者把字段解析为空。
为什么有些登录要先 GET 登录页
有些网站会在打开登录页时先写入初始 Cookie,并把 CSRF Token 放在隐藏表单字段里。此时直接 POST 用户名和密码会失败,正确链路是:用同一个 Session 先 GET 登录页,再带着初始 Cookie 和 CSRF Token 提交表单。下面用 beautifulsoup4 读取隐藏字段,并给出完整调用链。
import os
import requests
from bs4 import BeautifulSoup
BASE_URL = "https://example.com"
def fetch_private_data_with_csrf():
username = os.environ["DEMO_USERNAME"]
password = os.environ["DEMO_PASSWORD"]
with requests.Session() as session:
login_page = session.get(f"{BASE_URL}/login", timeout=10)
login_page.raise_for_status()
soup = BeautifulSoup(login_page.text, "html.parser")
csrf_input = soup.select_one("input[name='csrf_token']")
if csrf_input is None or not csrf_input.get("value"):
raise RuntimeError("没有找到登录页里的 CSRF Token")
login_response = session.post(
f"{BASE_URL}/login",
data={
"username": username,
"password": password,
"csrf_token": csrf_input["value"],
},
timeout=10,
)
login_response.raise_for_status()
data_response = session.get(
f"{BASE_URL}/api/private-data",
timeout=10,
)
data_response.raise_for_status()
return data_response.json()
字段名不一定叫 csrf_token,Token 也可能来自 Cookie、响应头或前置接口。不要照抄选择器;回到 Network 和登录页 DOM,确认网站真正使用的字段以及提交位置。
方案二:获取 Token 后设置 Authorization
前后端分离的网站经常通过登录接口返回 Token。课程第 64 节的示例使用 access_token 字段,并在后续请求中添加 Authorization: Bearer <token>。通用写法如下:
import os
import requests
BASE_URL = "https://example.com"
def fetch_with_token():
login_response = requests.post(
f"{BASE_URL}/api/login",
json={
"username": os.environ["DEMO_USERNAME"],
"password": os.environ["DEMO_PASSWORD"],
},
timeout=10,
)
login_response.raise_for_status()
login_data = login_response.json()
access_token = login_data.get("access_token")
if not access_token:
raise RuntimeError("登录响应中没有找到 access_token")
with requests.Session() as session:
# Bearer 后面必须有一个空格,且不要打印真实 Token
session.headers.update({
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
})
response = session.get(
f"{BASE_URL}/api/private-data",
timeout=10,
)
response.raise_for_status()
return response.json()
字段名和认证方案必须以真实接口为准。有的网站返回 token、id_token 或嵌套字段,有的使用自定义请求头,还有的会同时返回短期 Access Token 和长期 Refresh Token。不要猜字段名,也不要为了“看一眼”把 Token 打到日志里。
Token 登录还要考虑过期。单次学习脚本可以在 401 后重新登录;长期任务应按系统提供的刷新协议更新 Token,并给刷新动作设置次数上限。反复用错误密码登录,既可能触发账号锁定,也会给目标系统制造无意义压力。
方案三:Playwright 登录后把 Cookie 交给 requests
当登录页包含复杂 JavaScript、动态参数、验证码或多步跳转时,直接复现登录接口的成本可能很高。此时可以让 Playwright 完成你有权执行的正常登录流程,再把登录后的 Cookie 交给轻量的 Requests 采集接口。
import os
import requests
from requests.cookies import create_cookie
from playwright.sync_api import sync_playwright
LOGIN_URL = "https://example.com/login"
DATA_URL = "https://example.com/api/private-data"
def create_authenticated_session():
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=15_000)
page.locator("input[name='username']").fill(
os.environ["DEMO_USERNAME"]
)
page.locator("input[name='password']").fill(
os.environ["DEMO_PASSWORD"]
)
page.get_by_role("button", name="登录").click()
# 应替换成目标网站真实的登录成功条件
page.wait_for_url("**/dashboard", timeout=15_000)
# 只读取访问目标数据接口时会匹配的 Cookie
browser_cookies = context.cookies([DATA_URL])
browser_user_agent = page.evaluate("navigator.userAgent")
browser.close()
session = requests.Session()
session.headers.update({
"User-Agent": browser_user_agent,
"Accept": "application/json, text/plain, */*",
})
for item in browser_cookies:
# 复制常规 Cookie 的 Domain、Path、Secure 等基本属性
cookie_options = {
"name": item["name"],
"value": item["value"],
"domain": item["domain"],
"path": item.get("path", "/"),
"secure": item.get("secure", False),
}
if item.get("expires", -1) > 0:
cookie_options["expires"] = int(item["expires"])
cookie = create_cookie(
**cookie_options,
)
session.cookies.set_cookie(cookie)
return session
with create_authenticated_session() as session:
response = session.get(DATA_URL, timeout=10)
response.raise_for_status()
data = response.json()
page.wait_for_url("**/dashboard") 只是一个占位条件。真实项目里可以等待登录后的用户菜单出现、目标 URL 命中,或者登录接口返回成功。不要用固定 sleep(5) 猜页面什么时候完成,也不要点击后立刻读取 Cookie。
这段转换适合普通 Cookie,但不是浏览器 Cookie 状态的无损复制:Requests 无法等价表达 host-only 语义和 CHIPS 分区 Cookie。遇到分区 Cookie、复杂域名规则或迁移后仍失效的会话,应继续使用 Playwright 发请求,不要强行降级到 Requests。
只迁移 Cookie 也可能不够。部分网站把 Token 存在 localStorage、sessionStorage 或 IndexedDB,还可能依赖 CSRF Token、设备状态或一次性挑战。Playwright 的 Authentication 官方文档提供了 storage_state():默认保存 Cookie 和 localStorage;Playwright 1.51+ 可通过 indexed_db=True 显式包含 IndexedDB;sessionStorage 不在保存范围内,需要手动导出和注入。如果后续请求交给 Requests,则仍要根据真实接口,把必要 Token 转成对应请求头。
登录状态文件、Cookie 和 Token 都等同于临时账号凭证。即使文件只用于测试,也应放进忽略目录,限制读取权限,过期后及时删除,绝不能提交到公开仓库。
登录成功不能只看状态码 200
很多模拟登录脚本“没有报错”,却拿不到数据,原因是成功判断太宽松。登录页、验证码页和权限提示页同样可能返回 200。建议至少组合两个信号:
| 检查信号 | 能发现的问题 |
|---|---|
最终 URL 是否仍在 /login |
登录失败后静默跳回登录页 |
| 登录响应 JSON 的业务字段 | HTTP 成功但账号、验证码或参数校验失败 |
| Session 是否新增目标域名 Cookie | 登录响应没有建立会话 |
| 受保护接口能否返回预期字段 | Cookie 或 Token 存在,但没有真正生效 |
| 页面是否出现用户菜单、退出按钮 | Playwright 点击成功,但业务登录未完成 |
| 401/403 响应体的错误码 | 凭证缺失、过期、权限不足或 CSRF 校验失败 |
如果想观察 Requests 自动跟随了哪些跳转,可以检查 response.history;如果要看原始 302/303 响应,则临时关闭自动跳转:
response = session.post(
"https://example.com/login",
data={"username": "demo", "password": "example"},
allow_redirects=False,
timeout=10,
)
print(response.status_code)
print(response.headers.get("Location"))
示例里的账号密码仅用于说明参数位置。真实凭证仍应从环境变量、安全配置或凭证管理服务读取。
模拟登录后出现 401、403 怎么排查
先确认错误发生在“登录请求”还是“数据请求”,再按现象缩小范围:
| 现象 | 优先检查 |
|---|---|
| 登录接口 400 / 422 | 表单与 JSON 是否用反;字段名、CSRF 字段、编码是否正确 |
| 数据接口 401 | Cookie/Token 是否缺失或过期;Authorization 格式是否和浏览器一致 |
| 数据接口 403 | 账号权限、CSRF、Referer、会话绑定、访问频率和业务风控 |
| 返回 200 但内容是登录页 | 重定向被自动跟随;Cookie 的 Domain/Path 没匹配当前 URL |
| Playwright 正常,转 Requests 后失败 | 除 Cookie 外还依赖 localStorage Token、CSRF、User-Agent 或浏览器状态 |
| 第一页成功,后续突然失败 | 会话过期、Token 过期、频率过高或分页参数错误 |
排查时不要一上来就换代理、堆请求头或无限重试。先保存一次成功请求和一次失败请求,逐项比较 URL、Method、Payload、Cookie、Authorization、CSRF 字段和响应内容。请求头基础可以继续看HTTP 请求头与响应头实战,异常与超时处理可以看requests 异常处理完全指南。
如果你已经有 Cookie,却在验证码或翻页请求中遇到 403,可以对照requests Session:Cookie 自动保持、验证码重试与 403 排查。动态页面和浏览器自动化基础则在Playwright Python 教程里单独展开。
requests、Playwright 和 Token 登录怎么选
可以用下面这套顺序做决定:
- 登录接口清晰,表单或 JSON 可直接提交:优先用 Requests,资源占用小,调试链路短。
- 登录成功后由 Cookie 保持状态:整个任务复用同一个
requests.Session。 - 接口返回 Token:按真实请求设置认证头,不要默认所有 Token 都是 JWT 或 Bearer。
- 登录依赖复杂交互或动态脚本:用 Playwright 完成登录,再决定继续使用浏览器,还是把必要状态迁移给 Requests。
- 涉及验证码、短信、MFA 或人工确认:遵循网站授权流程,保留人工步骤或使用官方接口,不尝试绕过安全验证。
从维护成本看,能用稳定接口解决时,不必让浏览器长期运行;必须依赖浏览器状态时,也不要为了“轻量”强行迁移不完整的 Cookie。正确方案取决于登录状态到底由什么组成。
常见问题
requests.Session 和手动传 Cookie 有什么区别?
手动传 cookies={...} 适合一次性验证。Session 会持续接收服务器的新 Cookie,并在后续请求中自动匹配域名和路径,更适合登录、跳转、翻页这一整条链路。
拿到 Cookie 后为什么还是 403?
Cookie 可能已过期、作用域不匹配,或者网站还校验 CSRF Token、账号权限、Referer、设备状态和访问频率。先比较浏览器成功请求与 Python 失败请求,不要只看 Cookie 名称和值。
JWT 登录一定要先解码 Token 吗?
不需要。客户端通常只要按接口协议保存并携带 Token,服务端负责校验。解码负载不能证明签名有效,也不应被用来绕过过期或权限检查。
Playwright 登录状态可以长期复用吗?
取决于 Cookie 和 Token 的有效期,以及服务端是否会主动撤销会话。可以用 storage_state() 减少重复登录,但必须把状态文件当敏感凭证管理,并在失效后重新走合法登录流程。
模拟登录是否等于绕过登录?
不是。模拟登录是用程序执行你本来有权完成的认证流程。不要采集未获授权的数据,不要绕过验证码、MFA、付费访问或账号权限。项目开始前可先阅读爬虫法律边界与风险。
课程代码与延伸学习
本文的三条实战路径来自爬虫课程第 64 节:Cookie + Session、Playwright 登录态迁移,以及 Token + Authorization。课程代码会持续同步到 爬虫课程 Gitee 仓库,视频更新可在B 站课程主页查看。
继续练习时,建议自己完成三次验证:先用 Session 抓一页登录后数据,再用 Playwright 获取同一账号的 Cookie,最后对 Token 接口记录一次完整的登录与受保护请求。能清楚说出“凭证从哪里来、保存在哪里、后续怎么带”,模拟登录就真正跑通了。
Practice