ts
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(docker compose build:*)"
|
||||
"Bash(docker compose build:*)",
|
||||
"Bash(docker-compose down:*)",
|
||||
"Bash(docker compose:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+195
-52
@@ -527,93 +527,236 @@ async def check_login_status():
|
||||
async def verify_login():
|
||||
"""验证登录状态"""
|
||||
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:
|
||||
try:
|
||||
# 获取所有cookies
|
||||
cookies = await app_state.browser.context.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 = []
|
||||
for cookie in cookies:
|
||||
cookie_name = cookie.get('name', '').lower()
|
||||
cookie_value = cookie.get('value', '')
|
||||
|
||||
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:
|
||||
if any(keyword in cookie_name for keyword in ['session', 'token', 'auth', 'passport', 'sid']):
|
||||
session_cookies.append(cookie)
|
||||
print(f"[DEBUG] 找到session cookie: {cookie_name}")
|
||||
print(f"[DEBUG] ✓ 找到登录cookie: {cookie_name}")
|
||||
|
||||
if session_cookies:
|
||||
# 创建本地状态文件
|
||||
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)
|
||||
# 创建 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)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "登录成功",
|
||||
"logged_in": True,
|
||||
"login_time": auth_data["login_time"],
|
||||
"expire_time": auth_data["expire_time"],
|
||||
"expire_in_hours": 24
|
||||
}
|
||||
else:
|
||||
print("[DEBUG] 未找到任何session cookies")
|
||||
print("[DEBUG] ✓ 创建 auth_state 文件成功")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "登录成功",
|
||||
"logged_in": True,
|
||||
"login_time": auth_data["login_time"],
|
||||
"expire_time": auth_data["expire_time"],
|
||||
"expire_in_hours": 24
|
||||
}
|
||||
|
||||
if not session_cookies:
|
||||
print("[DEBUG] ✗ 未找到登录相关的 cookies")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] 浏览器cookie检查失败: {str(e)}")
|
||||
print(f"[DEBUG] ✗ 检查cookies失败: {str(e)}")
|
||||
pass
|
||||
|
||||
# 检查本地登录状态文件
|
||||
print("[DEBUG] 检查本地登录状态文件...")
|
||||
auth_file = os.path.join(config.DATA_DIR, "auth_state.json")
|
||||
if os.path.exists(auth_file):
|
||||
with open(auth_file, 'r', encoding='utf-8') as f:
|
||||
auth_data = json.load(f)
|
||||
try:
|
||||
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")
|
||||
expire_time = auth_data.get("expire_time")
|
||||
current_time = datetime.now().isoformat()
|
||||
# 检查登录是否过期
|
||||
login_time = auth_data.get("login_time")
|
||||
expire_time = auth_data.get("expire_time")
|
||||
current_time = datetime.now().isoformat()
|
||||
|
||||
if expire_time and current_time > expire_time:
|
||||
# 登录已过期,清除状态
|
||||
os.remove(auth_file)
|
||||
if expire_time and current_time > expire_time:
|
||||
# 登录已过期,清除状态
|
||||
print("[DEBUG] ✗ 登录已过期")
|
||||
os.remove(auth_file)
|
||||
return {
|
||||
"success": False,
|
||||
"message": "登录已过期,请重新登录"
|
||||
}
|
||||
|
||||
print("[DEBUG] ✓ 本地登录状态有效")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "登录已过期,请重新登录"
|
||||
"success": True,
|
||||
"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 {
|
||||
"success": True,
|
||||
"message": "登录成功",
|
||||
"logged_in": True,
|
||||
"login_time": login_time,
|
||||
"expire_time": expire_time,
|
||||
"expire_in_hours": 24
|
||||
}
|
||||
|
||||
print("[DEBUG] 所有检查都失败了")
|
||||
print("[DEBUG] ✗ 未找到任何登录状态")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "未找到登录状态,请先登录"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] 验证登录异常: {str(e)}")
|
||||
print(f"[DEBUG] ✗ 验证登录异常: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"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")
|
||||
async def logout():
|
||||
"""退出登录"""
|
||||
|
||||
@@ -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/` 目录
|
||||
Reference in New Issue
Block a user