Files
atomk-hermes-skills/skills/browser-automation/camoufox-google-2fa-login/SKILL.md
T

258 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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秒,轮询间隔不要太短。