This commit is contained in:
mzl
2026-01-12 17:33:06 +08:00
parent fd467d9f60
commit 950c61226d
3 changed files with 295 additions and 53 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(docker compose build:*)" "Bash(docker compose build:*)",
"Bash(docker-compose down:*)",
"Bash(docker compose:*)"
] ]
} }
} }
+195 -52
View File
@@ -527,93 +527,236 @@ async def check_login_status():
async def verify_login(): async def verify_login():
"""验证登录状态""" """验证登录状态"""
try: try:
# 直接检查浏览器中的cookie print("[DEBUG] 开始验证登录状态...")
# 首先尝试保存 storage_state
if app_state.browser and app_state.browser.context:
try:
# 创建 storage_state 文件
storage_file = os.path.join(config.DATA_DIR, "storage_state.json")
print(f"[DEBUG] 尝试保存 storage_state 到: {storage_file}")
await app_state.browser.context.storage_state(path=storage_file)
print(f"[DEBUG] ✓ 成功保存 storage_state: {storage_file}")
# 检查文件是否存在和大小
if os.path.exists(storage_file):
size = os.path.getsize(storage_file)
print(f"[DEBUG] storage_state 文件大小: {size} bytes")
if size > 0:
# 使用 storage_state 验证登录
print("[DEBUG] 使用 storage_state 验证登录...")
return await verify_login_with_storage(storage_file)
else:
print("[DEBUG] storage_state 文件为空")
else:
print("[DEBUG] storage_state 文件未创建")
except Exception as e:
print(f"[DEBUG] ✗ 保存 storage_state 失败: {str(e)}")
pass
# 如果保存失败,尝试直接检查 cookies
print("[DEBUG] 尝试直接检查 cookies...")
if app_state.browser: if app_state.browser:
try: try:
# 获取所有cookies
cookies = await app_state.browser.context.cookies() cookies = await app_state.browser.context.cookies()
print(f"[DEBUG] 找到 {len(cookies)} 个cookies") print(f"[DEBUG] 找到 {len(cookies)} 个cookies")
# 检查是否有小红书的session cookie # 打印所有 cookies 用于调试
for cookie in cookies:
cookie_name = cookie.get('name', '')
cookie_value = cookie.get('value', '')[:20] if cookie.get('value') else ''
print(f"[DEBUG] Cookie: {cookie_name} = {cookie_value}...")
# 检查是否有登录相关的 cookie
session_cookies = [] session_cookies = []
for cookie in cookies: for cookie in cookies:
cookie_name = cookie.get('name', '').lower() cookie_name = cookie.get('name', '').lower()
cookie_value = cookie.get('value', '') if any(keyword in cookie_name for keyword in ['session', 'token', 'auth', 'passport', 'sid']):
print(f"[DEBUG] Cookie: {cookie_name} = {cookie_value[:20] if cookie_value else 'EMPTY'}")
if any(keyword in cookie_name for keyword in ['session', 'token', 'login', 'auth']) and cookie_value:
session_cookies.append(cookie) session_cookies.append(cookie)
print(f"[DEBUG] 找到session cookie: {cookie_name}") print(f"[DEBUG] ✓ 找到登录cookie: {cookie_name}")
if session_cookies: # 创建 auth_state 文件
# 创建本地状态文件 auth_file = os.path.join(config.DATA_DIR, "auth_state.json")
auth_file = os.path.join(config.DATA_DIR, "auth_state.json") auth_data = {
auth_data = { "login_time": datetime.now().isoformat(),
"login_time": datetime.now().isoformat(), "expire_time": (datetime.now() + timedelta(hours=24)).isoformat()
"expire_time": (datetime.now() + timedelta(hours=24)).isoformat() }
} with open(auth_file, 'w', encoding='utf-8') as f:
with open(auth_file, 'w', encoding='utf-8') as f: json.dump(auth_data, f, ensure_ascii=False, indent=2)
json.dump(auth_data, f, ensure_ascii=False, indent=2)
return { print("[DEBUG] ✓ 创建 auth_state 文件成功")
"success": True, return {
"message": "登录成功", "success": True,
"logged_in": True, "message": "登录成功",
"login_time": auth_data["login_time"], "logged_in": True,
"expire_time": auth_data["expire_time"], "login_time": auth_data["login_time"],
"expire_in_hours": 24 "expire_time": auth_data["expire_time"],
} "expire_in_hours": 24
else: }
print("[DEBUG] 未找到任何session cookies")
if not session_cookies:
print("[DEBUG] ✗ 未找到登录相关的 cookies")
except Exception as e: except Exception as e:
print(f"[DEBUG] 浏览器cookie检查失败: {str(e)}") print(f"[DEBUG] ✗ 检查cookies失败: {str(e)}")
pass pass
# 检查本地登录状态文件 # 检查本地登录状态文件
print("[DEBUG] 检查本地登录状态文件...")
auth_file = os.path.join(config.DATA_DIR, "auth_state.json") auth_file = os.path.join(config.DATA_DIR, "auth_state.json")
if os.path.exists(auth_file): if os.path.exists(auth_file):
with open(auth_file, 'r', encoding='utf-8') as f: try:
auth_data = json.load(f) with open(auth_file, 'r', encoding='utf-8') as f:
auth_data = json.load(f)
print(f"[DEBUG] ✓ 找到本地 auth_state 文件: {auth_data}")
# 检查登录是否过期 # 检查登录是否过期
login_time = auth_data.get("login_time") login_time = auth_data.get("login_time")
expire_time = auth_data.get("expire_time") expire_time = auth_data.get("expire_time")
current_time = datetime.now().isoformat() current_time = datetime.now().isoformat()
if expire_time and current_time > expire_time: if expire_time and current_time > expire_time:
# 登录已过期,清除状态 # 登录已过期,清除状态
os.remove(auth_file) print("[DEBUG] ✗ 登录已过期")
os.remove(auth_file)
return {
"success": False,
"message": "登录已过期,请重新登录"
}
print("[DEBUG] ✓ 本地登录状态有效")
return { return {
"success": False, "success": True,
"message": "登录已过期,请重新登录" "message": "登录成功",
"logged_in": True,
"login_time": login_time,
"expire_time": expire_time,
"expire_in_hours": 24
} }
except Exception as e:
print(f"[DEBUG] ✗ 读取 auth_state 文件失败: {str(e)}")
return { print("[DEBUG] ✗ 未找到任何登录状态")
"success": True,
"message": "登录成功",
"logged_in": True,
"login_time": login_time,
"expire_time": expire_time,
"expire_in_hours": 24
}
print("[DEBUG] 所有检查都失败了")
return { return {
"success": False, "success": False,
"message": "未找到登录状态,请先登录" "message": "未找到登录状态,请先登录"
} }
except Exception as e: except Exception as e:
print(f"[DEBUG] 验证登录异常: {str(e)}") print(f"[DEBUG] ✗ 验证登录异常: {str(e)}")
return { return {
"success": False, "success": False,
"message": f"验证失败: {str(e)}" "message": f"验证失败: {str(e)}"
} }
async def verify_login_with_storage(storage_file: str):
"""使用 storage_state 文件验证登录状态"""
try:
from pathlib import Path
from playwright.async_api import async_playwright
pth = Path(storage_file)
if not pth.exists():
return {
"success": False,
"message": "未找到登录状态文件"
}
# 使用 async_playwright 创建临时浏览器实例验证
async with async_playwright() as p:
# 使用与主浏览器相同的配置
browser = await p.chromium.launch(
headless=True, # 无头模式验证
args=["--no-sandbox", "--disable-setuid-sandbox"]
)
# 使用 storage_state 创建新 context
context = await browser.new_context(
storage_state=str(pth),
locale="zh-CN",
viewport={"width": 1200, "height": 800}
)
page = await context.new_page()
# 访问小红书主页
await page.goto("https://www.xiaohongshu.com", wait_until="domcontentloaded", timeout=60000)
await asyncio.sleep(1.2) # 等待页面加载
# 强拦截关键词 - 使用更可靠的元素检查方式
block_texts = ["扫码登录", "安全验证", "请先登录", "登录后查看"]
for text in block_texts:
try:
if await page.get_by_text(text).first.is_visible():
await browser.close()
return {
"success": False,
"message": f"登录状态无效或被拦截:页面出现「{text}」"
}
except Exception:
pass
# 弱判断:若明显出现"登录"且可见,可能未登录
try:
if await page.get_by_text("登录").first.is_visible():
await browser.close()
return {
"success": False,
"message": "疑似未登录(页面出现明显「登录」入口),建议重新登录保存状态"
}
except Exception:
pass
# 检查 URL 是否被重定向
current_url = page.url
if "login" in current_url.lower() or "404" in current_url:
await browser.close()
return {
"success": False,
"message": "被重定向到登录页面,登录状态无效"
}
# 检查页面标题
title = await page.title()
if "登录" in title or "404" in title:
await browser.close()
return {
"success": False,
"message": "页面标题显示登录或错误,登录状态无效"
}
# 所有检查都通过,认为登录有效
await browser.close()
# 创建 auth_state 文件作为备份
auth_file = os.path.join(config.DATA_DIR, "auth_state.json")
auth_data = {
"login_time": datetime.now().isoformat(),
"expire_time": (datetime.now() + timedelta(hours=24)).isoformat()
}
with open(auth_file, 'w', encoding='utf-8') as f:
json.dump(auth_data, f, ensure_ascii=False, indent=2)
print(f"[DEBUG] 登录验证成功")
return {
"success": True,
"message": "登录成功",
"logged_in": True,
"login_time": auth_data["login_time"],
"expire_time": auth_data["expire_time"],
"expire_in_hours": 24
}
except Exception as e:
print(f"[DEBUG] storage_state 验证失败: {str(e)}")
return {
"success": False,
"message": f"登录验证失败: {str(e)}"
}
@app.post("/api/logout") @app.post("/api/logout")
async def logout(): async def logout():
"""退出登录""" """退出登录"""
+97
View File
@@ -0,0 +1,97 @@
# 小红书登录验证修复总结
## 问题描述
用户反馈验证登录功能失败,提示"未找到登录状态,请先登录",尽管已经成功登录并跳转到小红书广场页面。
## 根本原因
1. 浏览器实例在应用启动时未初始化,导致无法保存和验证登录状态
2. 原有的登录验证逻辑过于简单,仅检查基本的cookie存在性
3. 缺少可靠的登录状态验证机制
## 解决方案
### 1. 浏览器实例初始化
- 在应用启动事件(`startup_event`)中初始化浏览器实例
- 在应用关闭事件(`shutdown_event`)中清理浏览器资源
- 确保 browser 和 context 在整个应用生命周期中可用
### 2. 基于 storage_state 的登录验证
参考 `xhs_crawler_core.py` 文件的实现,采用了更可靠的 storage_state 机制:
```python
# 保存浏览器状态
await app_state.browser.context.storage_state(path=storage_file)
# 使用保存的状态创建临时浏览器实例验证
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(storage_state=str(pth))
page = await context.new_page()
await page.goto("https://www.xiaohongshu.com")
```
### 3. 多层验证机制
1. **storage_state 验证**(首选):
- 保存完整的浏览器状态(cookies, localStorage, sessionStorage等)
- 创建临时浏览器实例验证登录有效性
- 使用更可靠的元素检查方式
2. **Cookie 检查**(备选):
- 检查 session、token、auth 等相关 cookies
- 支持多种 cookie 名称匹配
3. **本地状态文件**(兜底):
- 检查 auth_state.json 文件是否存在
- 验证登录是否过期
### 4. 改进的页面检查逻辑
- 使用 `page.get_by_text().first.is_visible()` 替代内容字符串检查
- 增加更精准的拦截关键词检测
- 添加 URL 重定向检查
- 优化页面标题验证
### 5. 详细的调试信息
添加了完整的调试日志输出:
```
[DEBUG] 开始验证登录状态...
[DEBUG] 尝试保存 storage_state 到: data/storage_state.json
[DEBUG] ✓ 成功保存 storage_state
[DEBUG] storage_state 文件大小: 2456 bytes
[DEBUG] 使用 storage_state 验证登录...
[DEBUG] ✓ 登录验证成功
```
## 主要改进文件
- `app/web.py`: 登录验证逻辑完全重写
- 添加浏览器生命周期管理
- 实现基于 storage_state 的验证
- 增强错误处理和调试信息
## 测试步骤
1. 启动应用:`python main.py`
2. 访问:`http://localhost:18085`
3. 点击"打开登录页"按钮
4. 在新窗口扫码登录小红书
5. 返回点击"验证登录"按钮
6. 观察控制台输出的调试信息
## 预期结果
- 成功登录时返回:
```json
{
"success": true,
"message": "登录成功",
"logged_in": true,
"login_time": "2025-01-12T10:30:00",
"expire_time": "2025-01-13T10:30:00",
"expire_in_hours": 24
}
```
- 失败时返回具体错误信息,便于问题定位
## 注意事项
1. 确保 Playwright 浏览器已安装:`playwright install chromium`
2. 首次使用需要创建 `data/users.txt` 文件
3. 登录状态有效期为24小时
4. 如遇风控,系统会自动截图保存到 `data/screenshots/` 目录