本文只围绕三个问题展开:
- 如何稳定地获取浏览器中的 Cookie(以及相关会话状态)
- 如何在后续脚本中复用这份 Cookie,实现免登录访问
- 为什么这种方式在技术上是可行的(底层原理是什么)
一、如何获取 Cookie(认证采集脚本)
思路是:启动一个有界面的浏览器实例,让用户手动完成一次真实登录,然后由脚本在合适的时机导出当前会话状态并落盘。
下面是一个最小可用的"认证采集脚本"示例(Node.js + Playwright):
javascript
const { firefox } = require("playwright");
const fs = require("fs");
const path = require("path");
const AUTH_DIR = path.join(__dirname, "auth");
const AUTH_FILE = path.join(AUTH_DIR, "auth-state.json");
const BROWSER_EXECUTABLE = path.join(__dirname, "camoufox", "camoufox.exe");
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
async function captureAuth() {
ensureDir(AUTH_DIR);
const browser = await firefox.launch({
headless: false, // 必须有界面,用户需要手动登录
executablePath: BROWSER_EXECUTABLE, // 如需指定自带浏览器,可配置此路径
});
const context = await browser.newContext();
const page = await context.newPage();
// 打开登录入口页面,用户在页面中完成登录
await page.goto("https://example.com/login");
console.log("请在新打开的浏览器中完成登录,然后回到终端按 Enter 继续...");
await new Promise((resolve) => process.stdin.once("data", resolve));
// 导出当前上下文的完整会话状态(包含 Cookies / Storage)
const storageState = await context.storageState();
fs.writeFileSync(AUTH_FILE, JSON.stringify(storageState, null, 2), "utf-8");
console.log(`认证状态已写入: ${AUTH_FILE}`);
await browser.close();
process.exit(0);
}
captureAuth().catch((err) => {
console.error(err);
process.exit(1);
});
关键点说明:
-
上下文级别会话 :
browser.newContext()创建的是独立的浏览器上下文,类似一个轻量级"用户配置目录",其 Cookies、本地存储等和其他上下文隔离。 -
人工登录 + 自动导出 :
登录动作完全由用户在真实页面中完成,脚本只负责在用户确认后调用
context.storageState()导出状态。 -
会话状态文件化 :
storageState是一个普通 JS 对象,序列化为auth/auth-state.json后,就形成一个可长期复用的认证文件。
二、如何复用 Cookie(业务脚本)
有了认证文件之后,业务脚本只需要在创建浏览器上下文时,把这份状态注入进去,新的上下文就会继承原来的 Cookie 和存储数据。
下面是一个使用同一认证文件的业务脚本示例:
javascript
const { firefox } = require("playwright");
const fs = require("fs");
const path = require("path");
const AUTH_FILE = path.join(__dirname, "auth", "auth-state.json");
function loadStorageState() {
if (!fs.existsSync(AUTH_FILE)) {
throw new Error(`认证文件不存在: ${AUTH_FILE},请先运行采集脚本生成`);
}
const raw = fs.readFileSync(AUTH_FILE, "utf-8");
return JSON.parse(raw);
}
async function main() {
const storageState = loadStorageState();
// 业务脚本中通常可以使用无头模式
const browser = await firefox.launch({ headless: true });
// 关键点:在创建 context 的时候注入 storageState
const context = await browser.newContext({ storageState });
const page = await context.newPage();
// 此处直接访问需要登录态的页面
await page.goto("https://example.com/protected");
console.log("当前页标题:", await page.title());
await browser.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
复用过程的本质是:不再从空上下文开始,而是用一份"预先持久化"的会话状态初始化上下文。
三、为什么这样做可行
上面的流程之所以能工作,核心依赖三个技术点:
- HTTP Cookie 的工作机制
- 浏览器上下文(Browser Context)对会话状态的封装方式
- 自动化框架对"会话导出 / 会话注入"的支持(
storageState机制)
下面按这三个层次拆开说明。
3.1 HTTP Cookie 与会话状态
在 Web 体系中,服务端识别一个"已登录用户"的典型方式是:
- 用户在登录页面输入账号密码,发起登录请求;
- 服务端校验成功后,返回一个或多个包含会话信息的
Set-Cookie响应头,例如:
http
Set-Cookie: SESSION_ID=abcdef; Path=/; HttpOnly; Secure
- 浏览器将这些 Cookie 存入其 Cookie 存储中,并在后续访问同一域名(符合作用域和路径规则)时,自动携带这些 Cookie:
http
Cookie: SESSION_ID=abcdef
- 服务端根据 Cookie 中的会话标识(如
SESSION_ID)从自身会话存储中查找用户身份与权限,从而判断请求来自哪个登录用户。
因此,只要浏览器中持有与服务端会话对应的 Cookie 集合,并在后续请求中按同样的规则携带这些 Cookie,服务端就会认为这是同一个"已登录会话"。
3.2 浏览器上下文与会话隔离
现代自动化框架(如 Playwright)抽象了一个"浏览器上下文"(Browser Context)的概念:
- 每个上下文拥有独立的 Cookie 存储、本地存储、会话存储等;
- 同一浏览器实例下的多个上下文相互隔离,互不共享会话状态;
- 上下文可以看作一个轻量级的"浏览器用户配置路径"。
以 Playwright 为例:
javascript
const browser = await firefox.launch();
const contextA = await browser.newContext(); // 上下文 A
const contextB = await browser.newContext(); // 上下文 B
// A 中登录,不会影响 B
这意味着:只要我们能把某一个上下文的 Cookie 和相关存储完整导出,并在未来用相同的结构去创建新上下文,就能"克隆"这个上下文的会话状态。
3.3 storageState():会话导出的抽象
自动化框架提供了一个关键 API,用于导出当前上下文的会话状态,例如:
javascript
const storageState = await context.storageState();
这个 storageState 通常包含如下信息:
- 当前上下文中所有 Cookie(包含 name / value / domain / path / expires / secure / httpOnly 等字段)
- 按 origin 组织的 localStorage / sessionStorage 等数据
- 框架自身需要记录的与存储相关的元信息
从数据结构上看,它只是一个普通的 JavaScript 对象,可以安全地序列化为 JSON 文件并持久化:
javascript
fs.writeFileSync("auth-state.json", JSON.stringify(storageState, null, 2));
3.4 newContext({ storageState }):会话注入的抽象
在另一个脚本(或同一脚本的后续阶段),框架支持通过 storageState 初始化新的上下文:
javascript
const restoredState = JSON.parse(fs.readFileSync("auth-state.json", "utf-8"));
const context = await browser.newContext({
storageState: restoredState,
});
本质上,这一步执行了两件事情:
- 将
restoredState.cookies写入新上下文的 Cookie 存储区; - 将
restoredState.origins对应的数据写入到各个 origin 的本地存储空间中。
完成这一步之后,从新上下文发出的所有网络请求,将自动带上与原上下文完全一致的 Cookie 集合,因此在服务端看来:
- 这就是一个已经登录过的浏览器实例;
- 只要服务端的会话未过期,所有需要登录态的接口都可以直接访问。
3.5 为什么不需要在脚本中"模拟登录"
由于浏览器上下文的会话状态是可序列化和可恢复的,认证采集脚本只需要在"已经登录成功"的时刻做一次导出即可。
从那一刻起:
- 登录页面、登录表单、验证码、二次认证等流程全部被"折叠"进一次操作;
- 业务脚本无需再实现这段重复且高耦合的逻辑,只需要依赖认证文件即可。
这在工程上带来的好处是非常明显的:
- 登录流程的维护成本从"散落在每个脚本中"变为"集中在一个工具脚本中";
- 自动化任务本身完全与认证细节解耦,职责清晰;
- 对于风控敏感系统,可以降低频繁模拟登录的触发风险。
四、写在最后
在此基础上,可以按需进一步扩展诸如多账号管理、有效期检测、安全加密等能力,但本质仍然基于同一技术原理:会话状态是可序列化、可持久化、可注入的新上下文的。
关注 【松哥AI自动化】 公众号,每周获取深度技术解析,从源码角度彻底理解各种工具的实现原理。更重要的是,遇到技术难题时,直接联系我!我会根据你的具体情况,提供最适合的解决方案和技术指导。