Add browser-automation/camoufox-google-2fa-login

This commit is contained in:
2026-07-10 16:11:57 +08:00
parent 7549d08b01
commit d79017e2d8
@@ -0,0 +1,257 @@
---
name: camoufox-google-2fa-login
title: CamouFox Google 2FA登录
description: 使用CamouFox反检测浏览器登录Google账号,处理2FA短信验证码。关键:验证码与session绑定,必须保存storage_state在同一会话输入。
category: browser-automation
tags: [camoufox, google, 2fa, login, anti-detection]
---
# CamouFox Google 2FA 登录
## 核心问题
Google 2FA短信验证码是**session-bound**的。每次新的登录会话(新browser context)会自动使之前的验证码失效。因此:
- **错误做法1**:登录到2FA页 → 告诉用户等待 → 用户给码 → 开新session重新登录 → 输入验证码 → "验证码错误"(码已被新session失效)
- **错误做法2**:登录到2FA页 → `storage_state`保存 → 用户给码 → 新session用storage_state恢复 → Google返回400错误(TL参数已失效,storage_state无法恢复2FA中间状态)
- **正确做法**:登录到2FA页 → **后台脚本保持浏览器会话存活** → 轮询文件等待验证码 → 用户收到短信后写入文件 → **同一会话**内读取并输入验证码
## 依赖
```bash
pip install camoufox
playwright install # camoufox依赖playwright
```
## 完整流程
### 阶段1:登录到2FA验证码页面
```python
from camoufox.sync_api import Camoufox
import time, json
email = "my9commerce@gmail.com"
password = "Bingo2019Cc9"
STATE_FILE = "/tmp/gmail_2fa_state.json"
with Camoufox(headless=True, geoip=True, humanize=True) as browser:
page = browser.new_page()
# Step 1: Email
page.goto("https://accounts.google.com/signin", timeout=30000)
page.wait_for_load_state("networkidle", timeout=15000)
time.sleep(2)
page.query_selector("input[type=email]").fill(email)
time.sleep(1)
page.query_selector("#identifierNext").click()
time.sleep(5)
# Step 2: Password
page.query_selector("input[type=password]").fill(password)
time.sleep(1)
page.query_selector("#passwordNext").click()
time.sleep(6)
# Step 3: 2FA - click SMS option
page.evaluate('''() => {
const items = document.querySelectorAll("[role=link]");
for (const item of items) {
const t = (item.innerText || "");
if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) {
item.click(); return;
}
}
}''')
time.sleep(6)
# Verify we're on code entry page
body = page.evaluate("document.body.innerText")
if "输入验证码" in body or "Enter the code" in body:
# 保存完整浏览器状态(关键!)
page.context.storage_state(path=STATE_FILE)
print("SMS code sent! Waiting for user to provide the code.")
print(f"State saved to {STATE_FILE}")
else:
print("Failed to reach code entry page")
```
### 正确做法:后台脚本保持会话 + 文件轮询
**关键**:必须在同一个浏览器会话内输入验证码。用后台脚本保持CamouFox进程存活,通过文件信号机制传递验证码。
#### 后台登录脚本(gmail_login_wait.py
```python
#!/usr/bin/env python3
import json, time, os, sys
from camoufox.sync_api import Camoufox
email = "my9commerce@gmail.com"
password = "YOUR_PASSWORD"
CODE_FILE = "/tmp/gmail_code.txt"
SIGNAL_FILE = "/tmp/gmail_signal.txt"
# Clean old files
for f in [CODE_FILE, SIGNAL_FILE]:
if os.path.exists(f): os.remove(f)
print("STARTING", flush=True)
with Camoufox(headless=True, geoip=True, humanize=True) as browser:
page = browser.new_page()
# Step 1: Email
page.goto("https://accounts.google.com/signin", timeout=30000)
page.wait_for_load_state("networkidle", timeout=15000)
time.sleep(2)
page.query_selector("input[type=email]").fill(email)
time.sleep(1)
page.query_selector("#identifierNext").click()
time.sleep(5)
# Step 2: Password
page.query_selector("input[type=password]").fill(password)
time.sleep(1)
page.query_selector("#passwordNext").click()
time.sleep(6)
# Step 3: 2FA - click SMS option
page.evaluate('''() => {
const items = document.querySelectorAll("[role=link]");
for (const item of items) {
const t = (item.innerText || "");
if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) {
item.click(); return;
}
}
}''')
time.sleep(6)
# Verify on code entry page
body = page.evaluate("document.body.innerText")
if "输入验证码" not in body and "Enter the code" not in body:
print(f"NOT_ON_CODE_PAGE", flush=True)
sys.exit(1)
# Signal ready - SMS has been sent
print("WAITING_FOR_CODE", flush=True)
with open(SIGNAL_FILE, "w") as f:
f.write("READY")
# Poll for code file (max 120 seconds)
code = None
for i in range(120):
if os.path.exists(CODE_FILE):
with open(CODE_FILE, "r") as f:
code = f.read().strip()
if len(code) == 6 and code.isdigit():
print(f"GOT_CODE: {code}", flush=True)
break
time.sleep(1)
else:
print("TIMEOUT", flush=True)
sys.exit(1)
# Step 4: Enter code IN SAME SESSION
pin = page.query_selector("#idvPin")
if pin:
pin.fill(code)
time.sleep(1)
page.evaluate('''() => {
const btns = document.querySelectorAll("button");
for (const b of btns) {
const t = (b.innerText || "").trim();
if (t === "下一步" || t === "Next") { b.click(); return; }
}
}''')
time.sleep(10)
body2 = page.evaluate("document.body.innerText")
if "错误" in body2 or "wrong" in body2.lower():
print("CODE_REJECTED", flush=True)
sys.exit(1)
page.goto("https://mail.google.com", timeout=20000)
time.sleep(5)
if "mail.google.com" in page.url:
print("LOGIN_SUCCESS", flush=True)
cookies = page.context.cookies()
with open("/home/ubuntu/.hermes/cookies/gmail_cookies.json", "w") as f:
json.dump(cookies, f)
print("COOKIES_SAVED", flush=True)
else:
print(f"LOGIN_FAILED: {page.url}", flush=True)
```
#### 启动与交互流程
```bash
# 1. 后台启动脚本(注意 -u 参数确保无缓冲输出)
/home/ubuntu/.hermes/hermes-agent/venv/bin/python3 -u gmail_login_wait.py &
# 2. 等待信号文件
while [ ! -f /tmp/gmail_signal.txt ]; do sleep 2; done
echo "Ready for code"
# 3. 用户确认收到短信后,写入验证码
echo "123456" > /tmp/gmail_code.txt
# 4. 检查结果
# 脚本会输出 LOGIN_SUCCESS 或 CODE_REJECTED
```
**重要时序**:脚本启动后Google会发送新验证码。用户必须等**这个脚本触发的**短信,而不是之前任何会话的验证码。
1. **必须用CamouFox**:普通headless Chromium会被Google拦截,重定向到`/signin/rejected`("此浏览器或应用可能不安全")。CamouFox的指纹伪装+geoip+humanize能绕过检测。
2. **2FA验证码是session-bound**:新登录会话会使旧验证码失效。必须在同一session内输入验证码,或用`storage_state`保存/恢复session。
3. **2FA选择页面的点击**Google 2FA选择页面用`[role="link"]`元素,不是按钮。需要通过文本内容("验证码"或手机尾号)定位。
4. **PIN输入框**`#idvPin`type为`tel`name为`Pin`
5. **storage_state无法恢复2FA中间状态**:实测发现用storage_state恢复session后访问2FA页面返回400错误。Google的TL参数是临时的、一次性的。**必须保持原始浏览器进程存活**,不能关掉后重新打开。
6. **等待时间**:Google的页面切换需要较长等待(5-6秒),网络不稳定时可能需要更长。
## 常见问题
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| "此浏览器或应用可能不安全" | 普通Chromium被Google反自动化检测 | 使用CamouFox(headless=True, geoip=True, humanize=True) |
| "验证码错误" | 验证码来自不同的登录session | 用storage_state保存/恢复session,在同一session输入 |
| 2FA选择页面点击无反应 | 点击了外层容器而非内部链接 | 用`[role="link"]`定位,通过innerText匹配 |
| 页面停留在challenge/selection | SMS选项点击未触发跳转 | 确认JS evaluate中click生效,增加等待时间 |
| storage_state恢复后返回400错误 | Google TL参数是临时的一次性的,storage_state无法恢复2FA中间状态 | **不要用storage_state恢复**,用后台脚本保持浏览器进程存活,通过文件轮询传递验证码 |
## CamouFox启动参数
```python
with Camoufox(
headless=True, # 无头模式(服务器环境必须)
geoip=True, # 自动匹配IP地理位置的指纹
humanize=True, # 模拟人类操作模式
) as browser:
page = browser.new_page()
```
## Python路径
系统上CamouFox安装在Hermes venv中:
```
/home/ubuntu/.hermes/hermes-agent/venv/bin/python3
```
注意:`/root/`路径可能权限不足,用`/home/ubuntu/`路径。
## 经验教训(踩坑记录)
1. **验证码时序陷阱**:用户给的是上一轮会话的验证码,但新一轮登录已经使旧码失效。每次新CamouFox会话 = 新登录 = 新验证码。**必须等用户确认收到当前会话的短信后再输入**。
2. **execute_code无法保持会话**execute_code每次都是新进程,无法保持浏览器会话。必须用terminal(background=true)启动脚本,通过文件信号机制(SIGNAL_FILE/CODE_FILE)协调。
3. **脚本输出需要flush**Python后台脚本必须用`print(..., flush=True)``python3 -u`参数,否则stdout缓冲导致无法实时看到输出。
4. **CamouFox启动较慢**:从启动到2FA页面约需30-40秒,轮询间隔不要太短。