PAYATHON 2026

Playwright 登录后将 Cookie 传给 requests,为何仍返回 403?

数据小陈

可以将 Playwright 的 Cookie 转移到 requests.Session(),但复制 Cookie 并不能保证 API 接受请求。现有代码无法证明服务器拒绝的是 session;CSRF 校验、接口权限或其他请求条件也可能导致 403 Forbidden。

建议先停止清理 Cookie 值,保留 Cookie 的作用域,再对比浏览器中同一个 API 的成功请求。登录后跳转到首页,并不能说明该 session 有权访问目标 API。

原代码中需要检查的地方

原示例将 Cookie 转成 {name: value} 字典,会丢失 domain、path、secure 和过期时间。作用域不同的同名 Cookie 还会互相覆盖。正确导入 Cookie 后,requests.Session() 可以解决这部分问题,但不会自动补齐其他认证条件。

原代码还假设 csrf-token 请求头可以直接使用 CSRF_COOKIE 的值。这个对应关系由网站决定:有的网站需要解码后的 Cookie 值,有的网站从页面或其他响应中提供 token,也有的网站同时检查 Origin、Referer 等信息。设置了同名请求头,不代表它符合服务器的校验规则。

先确认浏览器中的 API 请求是否成功

在 Playwright 登录后的页面中,通过浏览器开发者工具的 Network 面板查看目标 API:

  1. 确认请求的 URL、HTTP 方法、查询参数和请求体与 Python 请求一致。
  2. 确认这个 API 在浏览器中确实返回成功,而不只是首页能打开。
  3. 查看实际发送的 Cookie、csrf-token、Authorization、Origin 和 Referer,确认哪些字段参与认证。
  4. 检查 API 是否位于另一个子域名,以及登录后是否还需要访问页面来初始化相关 token。

如果浏览器请求同样返回 403,应先检查账号权限或接口要求。此时仅调整 Cookie 导出方式通常没有帮助。

下面使用同步 Playwright API,假设 context 和 page 已存在且登录完成。示例仍沿用原问题中“CSRF header 直接取自 Cookie”的假设,实际使用前需要通过成功请求确认这一对应关系。

import requests

api_url = "https://example.com/api/data"

# 只取得适用于目标 URL 的 Cookie,保留原始值和作用域。
cookies = context.cookies([api_url])

session = requests.Session()

for cookie in cookies:
    options = {
        "domain": cookie["domain"],
        "path": cookie["path"],
        "secure": cookie["secure"],
    }

    expires = cookie.get("expires", -1)
    if expires > 0:
        options["expires"] = int(expires)

    session.cookies.set(
        cookie["name"],
        cookie["value"],
        **options,
    )

headers = {
    "accept": "application/json",
    "user-agent": page.evaluate("navigator.userAgent"),
}

# 仅在确认网站使用这一对应关系后采用此逻辑。
csrf_cookies = [c for c in cookies if c["name"] == "CSRF_COOKIE"]
if len(csrf_cookies) > 1:
    raise ValueError("存在多个同名 CSRF Cookie,需要确认服务器使用哪一个")
if csrf_cookies:
    headers["csrf-token"] = csrf_cookies[0]["value"]

response = session.get(
    api_url,
    headers=headers,
    timeout=30,
    allow_redirects=False,
)

print("status:", response.status_code)
print("content-type:", response.headers.get("content-type"))
print("location:", response.headers.get("location"))

使用页面实际的 User-Agent 可以减少一项请求差异,但相同的 User-Agent 不能保证服务器将两个客户端视为相同环境。

示例关闭了重定向,以便观察原始响应。如果接口先跳转到登录页,自动跟随重定向可能会掩盖这一线索。查看响应正文也有帮助,例如可以区分 CSRF 错误、权限错误和访问验证页面。不要在公开日志中输出完整 Cookie 或 token。

不要根据显示效果删除引号

"\"value\"" 可能是字符串经过 JSON 编码后的显示形式,也可能表示 Cookie 本身确实包含引号,不能仅凭展示形式判断。

可以在本地用 repr(cookie["value"]) 检查字符串内容,再与浏览器实际发送的请求比较。不要公开检查结果中的凭据。

原问题中的 clean() 会执行 strip()、json.loads() 和引号删除。这些操作可能改变服务器需要的原始值,导致签名或 token 比较失败。应先原样转移 Cookie 值,只有网站明确要求转换时,才处理对应字段。

还要区分两个位置:即使 CSRF header 需要去掉外层引号,也不意味着发送到 Cookie 请求头中的值需要同步修改。它们可能有不同的格式要求。另外,所贴 clean() 片段缺少 Python 缩进,直接运行会先出现语法问题。

可以用 context.request 缩小排查范围

如果只需要在登录后调用接口,可以先尝试 context.request。它与对应的 BrowserContext 共享 Cookie 存储,可以省去手动导出和导入的步骤。

api_url = "https://example.com/api/data"

# headers 按浏览器成功请求设置,包含接口实际要求的 CSRF 等字段。
api_response = context.request.get(
    api_url,
    headers=headers,
    timeout=30000,
)

print(api_response.status)

这里 Playwright 的 timeout 单位是毫秒,前面 requests 的 timeout 单位是秒。

context.request 不会自动补齐页面请求中的所有 header 或动态 token,也不等同于页面内发出的请求。如果它成功而 requests 失败,可以继续检查手动迁移 Cookie 的过程或客户端差异;如果两者都失败,应重新核对接口要求和浏览器中的成功请求。

仍然返回 403 时如何判断

完成上述对比后,可以根据响应信息定位原因:

  • 响应明确提示 CSRF 错误:核对 token 的来源、编码、有效期,以及它是否与当前 session 配套。
  • 响应要求重新登录:核对目标域名对应的 session Cookie、过期时间和登录初始化流程。
  • 响应提示权限不足:确认账号是否有权调用该接口,复制 Cookie 无法增加权限。
  • 响应是访问验证页面:可能存在客户端环境校验,普通 requests 未必适用,应考虑网站提供的正式 API 或受支持的访问方式。

现有材料不足以确定具体原因。判断时应重点参考同一 API 在浏览器中的成功请求,以及服务器返回的具体错误信息。

备注:内容仅供参考。