登录态持久化:Cookie 生命周期与验证码登录

0 阅读8分钟

第4节提到一个事故:Cookie 失效后脚本静默失败了一整周。这一节彻底解决登录态问题——从 Cookie 保存到失效检测到验证码兜底,打造一套登录态不丢、丢了立刻知道、知道就能恢复的完整方案。


为什么登录态是第一道坎

大多数需要自动化的平台都有登录墙。OUCHN 国开学习网尤其麻烦:

特性影响
短信验证码登录无法全自动,必须人工介入
Cookie 约 7-10 天过期每周至少手动登录一次
登录后重定向Cookie 失效后被静默跳转到登录页,不报错
多终端使用家里和办公室的 Cookie 需要同步

如果只是每次都手动登录,那不算自动化。核心目标是:把人工登录的频率从每天一次降到每周一次,且失效时立刻感知


Cookie 持久化:保存与加载

基本流程

首次运行:人工登录 → 保存 Cookie 到 JSON → 后续加载复用
         ↓                                    ↓
    start_edge_debug.bat              cookie_file = "ouchn_cookies.json"

保存 Cookie

from playwright.sync_api import sync_playwright
import json

CDP_URL = "http://localhost:9222"
COOKIE_FILE = "ouchn_cookies.json"

def save_cookies(context):
    """从浏览器上下文提取 Cookie,保存到文件"""
    cookies = context.cookies()
    with open(COOKIE_FILE, "w", encoding="utf-8") as f:
        json.dump(cookies, f, ensure_ascii=False, indent=2)
    print(f"✅ Cookie 已保存: {len(cookies)} 条")

加载 Cookie

def load_cookies(context):
    """从文件加载 Cookie 到浏览器上下文"""
    from pathlib import Path
    if not Path(COOKIE_FILE).exists():
        return False

    with open(COOKIE_FILE, "r", encoding="utf-8") as f:
        cookies = json.load(f)

    context.add_cookies(cookies)
    print(f"✅ Cookie 已加载: {len(cookies)} 条")
    return True

完整连接流程

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(CDP_URL)
    context = browser.contexts[0] if browser.contexts else browser.new_context()

    # 1. 先加载已有 Cookie
    load_cookies(context)

    # 2. 访问目标页面,检测登录态
    page = context.pages[0] if context.pages else context.new_page()
    page.goto("https://lms.ouchn.cn", wait_until="domcontentloaded")
    page.wait_for_timeout(3000)

    # 3. 检查是否被重定向到登录页
    if "login" in page.url or "auth" in page.url:
        print("❌ Cookie 已失效,需要重新登录")
        # 进入人工登录等待流程(见下文)
    else:
        print("✅ 登录态有效")

    # 4. 任务完成后保存最新 Cookie(刷新过期时间)
    save_cookies(context)

关键:每次运行结束都重新保存 Cookie。有些平台的 Cookie 会在每次访问时刷新过期时间,持续保存就能延长有效期。


Cookie 失效检测

第4节的"坑3"——Cookie 失效静默失败一周——根因就是没有检测逻辑。脚本被重定向到登录页后,拿不到数据就正常退出了,exit code 0,看起来一切正常。

检测函数

async def check_logged_in(page):
    """检测当前页面是否处于登录状态"""
    await page.goto("https://lms.ouchn.cn", wait_until="domcontentloaded")
    await asyncio.sleep(3)

    # 方法1:检查 URL 是否包含 login/auth
    if "login" in page.url or "auth" in page.url:
        return False

    # 方法2:检查页面是否有登录表单
    has_login_form = await page.evaluate("""
        () => {
            return !!document.querySelector(
                'input[type="password"], .login-form, #login'
            );
        }
    """)
    if has_login_form:
        return False

    # 方法3:检查是否有用户信息元素(已登录才有)
    has_user_info = await page.evaluate("""
        () => {
            return !!document.querySelector(
                '.user-info, .user-name, [class*="avatar"]'
            );
        }
    """)

    return has_user_info

在主流程中使用

async def main():
    # ... 连接浏览器、加载 Cookie ...

    if not await check_logged_in(page):
        print("❌ 登录态失效!")
        await wait_for_manual_login(page, context)
        # 人工登录完成后继续

    # 正常执行任务
    await run_tasks(page)

三层检测的原因:有些平台重定向后 URL 不变(前端路由),有些登录页没有 password 输入框(验证码扫码),只靠一种方法会漏检。


短信验证码:人工登录兜底

OUCHN 平台需要手机短信验证码,这一步无法自动化。但可以做到:检测到失效后暂停,等用户在浏览器里手动登录,登录完成后自动继续

async def wait_for_manual_login(page, context, timeout=300):
    """
    等待用户手动登录。
    timeout: 最大等待秒数(默认5分钟)
    """
    print("=" * 50)
    print("🔑 登录态已失效,请在浏览器中手动登录")
    print(f"   调试浏览器端口: {CDP_URL}")
    print(f"   等待超时: {timeout} 秒")
    print("=" * 50)

    # 确保浏览器停在登录页
    await page.goto("https://lms.ouchn.cn/#/login", wait_until="domcontentloaded")

    # 轮询检测登录状态
    for i in range(timeout // 5):
        await asyncio.sleep(5)
        if await check_logged_in(page):
            print("✅ 检测到登录成功!保存 Cookie...")
            save_cookies(context)
            return True
        # 每30秒提示一次
        if i % 6 == 0 and i > 0:
            print(f"   仍在等待... ({i * 5}秒)")

    print("❌ 等待超时,请稍后重试")
    return False

用户体验

[08:40] 定时任务启动
[08:40] ❌ 登录态失效!
[08:40] 🔑 请在浏览器中手动登录
[08:40]    调试浏览器端口: http://localhost:9222
[08:40]    等待超时: 300 秒

  (用户打开浏览器,输入账号密码,收到短信验证码,登录)

[08:43] ✅ 检测到登录成功!保存 Cookie...
[08:43] 开始执行任务...

设计原则:自动化不是消除人工,而是把人工降到最低。每周手动登录一次,比每天手动点 100 页强得多。


调试浏览器启动脚本

Cookie 持久化的前提是有一个带登录态的调试浏览器。每次手动启动太麻烦,写个批处理:

@echo off
REM start_edge_debug.bat — 启动带调试端口的 Edge

set EDGE_PATH="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
set USER_DATA="C:\EdgeDebugProfile"
set PORT=9222

REM 如果端口已被占用,直接退出
netstat -ano | findstr ":%PORT%" >nul 2>&1
if %errorlevel%==0 (
    echo 调试浏览器已在运行 (端口 %PORT%)
    exit /b 0
)

echo 启动调试 Edge (端口 %PORT%)...
start "" %EDGE_PATH% --remote-debugging-port=%PORT% --user-data-dir=%USER_DATA%
timeout /t 3 >nul
echo ✅ 调试浏览器已启动

--user-data-dir 指定独立的用户目录,避免污染日常浏览器。Cookie 和登录态存在这个目录里,重启不丢。


多终端 Cookie 同步

家里和办公室各有一台电脑,两边的 Cookie 可能不同步。方案:通过乐享知识库共享 Cookie 文件。

async def sync_cookies_via_lexiang(action="upload"):
    """
    通过乐享知识库同步 Cookie。
    action: "upload" 上传本机 Cookie / "download" 下载云端 Cookie
    """
    if action == "upload":
        # 读取本地 Cookie
        with open(COOKIE_FILE, "r", encoding="utf-8") as f:
            cookies = f.read()
        # 上传到乐享(AI 调用 lexiang MCP)
        print(f"上传 Cookie 到乐享 ({len(cookies)} 字符)")
        # 实际操作:告诉 WorkBuddy "把 ouchn_cookies.json 上传到乐享"

    elif action == "download":
        # 从乐享下载 Cookie
        print("从乐享下载 Cookie...")
        # 实际操作:告诉 WorkBuddy "从乐享下载 ouchn_cookies.json"
        # 下载后覆盖本地文件
        if Path(COOKIE_FILE).exists():
            print("✅ Cookie 已同步")
            return True
    return False

同步时机

场景操作
本机登录成功后upload(把新 Cookie 推到云端)
换电脑首次运行前download(拉取最新 Cookie)
Cookie 失效时download(试试云端的是否还有效)

注意:Cookie 含敏感信息,只在私有乐享空间同步,不要用公开链接。


四个真实踩坑

坑 1 — Cookie 保存了但加载没用

现象:保存了 Cookie 文件,下次加载后访问页面还是未登录。

原因context.add_cookies() 要求 Cookie 格式必须包含 namevaluedomainpath 四个字段。Playwright 的 context.cookies() 返回的格式是兼容的,但如果你手动编辑过 JSON,可能漏掉字段。

修复:加载前校验格式:

def load_cookies_safe(context):
    with open(COOKIE_FILE, "r", encoding="utf-8") as f:
        cookies = json.load(f)

    # 过滤掉缺少必要字段的 Cookie
    valid = [c for c in cookies
             if all(k in c for k in ("name", "value", "domain", "path"))]

    if len(valid) < len(cookies):
        print(f"⚠️ 过滤了 {len(cookies) - len(valid)} 条无效 Cookie")

    context.add_cookies(valid)

坑 2 — Cookie 有效但页面还是跳登录

现象:Cookie 没过期,但访问页面还是被跳到登录页。

原因:有些平台除了 Cookie 还依赖 Session(服务端状态)。Session 过期了,Cookie 还在,但服务端不认。

修复:Cookie 不是万能的。检测到跳登录页就触发人工登录流程,别只依赖 Cookie 时间戳判断。

坑 3 — 调试浏览器关闭后 Cookie 丢了

现象:关了调试浏览器再开,之前的登录态没了。

原因--user-data-dir 路径不对,或者被清理了。

修复:确保 --user-data-dir 指向一个不会被清理的稳定目录。不要用临时目录。

坑 4 — 两个终端同时跑,Cookie 互相覆盖

现象:家里和办公室同时跑脚本,一边的 Cookie 把另一边覆盖了。

修复:避免同时运行。定时任务错开时间。或者每台机器用独立的 Cookie 文件,不强制同步。


小结

学会的要点
Cookie 持久化context.cookies() 保存 → add_cookies() 加载
失效检测URL 检测 + 登录表单检测 + 用户元素检测,三层防漏
验证码兜底检测到失效 → 暂停等待人工登录 → 检测到成功 → 继续
调试浏览器--remote-debugging-port + --user-data-dir 独立目录
多终端同步乐享上传/下载 Cookie,登录后上传,换机前下载
踩坑预防格式校验、Session 依赖、目录稳定、避免同时运行

下一篇预告

第 6 节:Angular 页面自动化进阶——Select2/MultiSelect 组件处理、scope 注入通用模式、$apply() 触发脏检查、表单提交与按钮匹配。解决"DOM 操作无效"的终极方案。


📺 系列文章:

本文首发于稀土掘金。欢迎关注后续连载。