183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from playwright.async_api import async_playwright
|
|||
|
|
from utils.stealth import stealth_async
|
|||
|
|
|
|||
|
|
AUTH_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'auth')
|
|||
|
|
LOGIN_URL = "https://sso.weidian.com/login/index.php"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_auth_path(account_id):
|
|||
|
|
os.makedirs(AUTH_DIR, exist_ok=True)
|
|||
|
|
return os.path.join(AUTH_DIR, f'auth_state_{account_id}.json')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def has_auth(account_id):
|
|||
|
|
path = get_auth_path(account_id)
|
|||
|
|
return os.path.exists(path) and os.path.getsize(path) > 10
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def login_with_password(account_id, phone, password):
|
|||
|
|
"""
|
|||
|
|
用 Playwright 模拟浏览器登录微店,通过监听 API 响应提取 cookie。
|
|||
|
|
流程:
|
|||
|
|
1. 打开登录页
|
|||
|
|
2. 点击 #login_init_by_login 进入登录表单
|
|||
|
|
3. 点击"账号密码登录" tab
|
|||
|
|
4. 填写手机号、密码
|
|||
|
|
5. 点击 #login_pwd_submit
|
|||
|
|
6. 监听 /user/login 响应,从中提取 cookie 并保存
|
|||
|
|
"""
|
|||
|
|
login_result = {'success': False, 'msg': '登录超时', 'cookies': []}
|
|||
|
|
|
|||
|
|
p = await async_playwright().start()
|
|||
|
|
browser = await p.chromium.launch(
|
|||
|
|
headless=True, args=['--disable-gpu', '--no-sandbox']
|
|||
|
|
)
|
|||
|
|
device = p.devices['iPhone 13']
|
|||
|
|
context = await browser.new_context(**device)
|
|||
|
|
page = await context.new_page()
|
|||
|
|
await stealth_async(page)
|
|||
|
|
|
|||
|
|
# 监听登录接口响应
|
|||
|
|
async def on_response(response):
|
|||
|
|
if 'user/login' in response.url and response.status == 200:
|
|||
|
|
try:
|
|||
|
|
data = await response.json()
|
|||
|
|
status = data.get('status', {})
|
|||
|
|
if status.get('status_code') == 0:
|
|||
|
|
login_result['success'] = True
|
|||
|
|
login_result['msg'] = '登录成功'
|
|||
|
|
login_result['cookies'] = data.get('result', {}).get('cookie', [])
|
|||
|
|
else:
|
|||
|
|
login_result['msg'] = f"登录失败: {status.get('status_reason', '未知错误')}"
|
|||
|
|
except Exception as e:
|
|||
|
|
login_result['msg'] = f"解析登录响应失败: {e}"
|
|||
|
|
|
|||
|
|
page.on("response", on_response)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
await page.goto(LOGIN_URL, wait_until='networkidle', timeout=15000)
|
|||
|
|
await asyncio.sleep(1)
|
|||
|
|
|
|||
|
|
# 点击"登录"进入表单
|
|||
|
|
await page.locator('#login_init_by_login').click(timeout=5000)
|
|||
|
|
await asyncio.sleep(1.5)
|
|||
|
|
|
|||
|
|
# 点击"账号密码登录" tab
|
|||
|
|
try:
|
|||
|
|
await page.locator('h4.login_content_h4 span', has_text="账号密码登录").click(timeout=3000)
|
|||
|
|
await asyncio.sleep(0.5)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 填写手机号(逐字输入,触发 JS 事件)
|
|||
|
|
phone_input = page.locator('input[placeholder*="手机号"]').first
|
|||
|
|
await phone_input.click()
|
|||
|
|
await phone_input.fill("")
|
|||
|
|
await page.keyboard.type(phone, delay=50)
|
|||
|
|
|
|||
|
|
# 填写密码
|
|||
|
|
pwd_input = page.locator('input[placeholder*="登录密码"], input[type="password"]').first
|
|||
|
|
await pwd_input.click()
|
|||
|
|
await pwd_input.fill("")
|
|||
|
|
await page.keyboard.type(password, delay=50)
|
|||
|
|
await asyncio.sleep(0.5)
|
|||
|
|
|
|||
|
|
# 点击登录
|
|||
|
|
await page.locator('#login_pwd_submit').click(timeout=5000)
|
|||
|
|
|
|||
|
|
# 等待 API 响应
|
|||
|
|
await asyncio.sleep(5)
|
|||
|
|
|
|||
|
|
if login_result['success'] and login_result['cookies']:
|
|||
|
|
# 从 API 响应中提取 cookie,写入 context
|
|||
|
|
for c in login_result['cookies']:
|
|||
|
|
await context.add_cookies([{
|
|||
|
|
"name": c.get("name", ""),
|
|||
|
|
"value": c.get("value", ""),
|
|||
|
|
"domain": c.get("domain", ".weidian.com"),
|
|||
|
|
"path": c.get("path", "/"),
|
|||
|
|
"httpOnly": c.get("httpOnly", False),
|
|||
|
|
"secure": c.get("secure", False),
|
|||
|
|
"sameSite": "Lax",
|
|||
|
|
}])
|
|||
|
|
|
|||
|
|
# 保存完整的 storage_state
|
|||
|
|
auth_path = get_auth_path(account_id)
|
|||
|
|
await context.storage_state(path=auth_path)
|
|||
|
|
return True, "登录成功"
|
|||
|
|
|
|||
|
|
return False, login_result['msg']
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"登录过程出错: {e}"
|
|||
|
|
finally:
|
|||
|
|
await browser.close()
|
|||
|
|
await p.stop()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def login_with_api(account_id, phone, password):
|
|||
|
|
"""
|
|||
|
|
通过微店 SSO API 直接登录(备选方案,速度快但更容易触发风控)。
|
|||
|
|
"""
|
|||
|
|
import aiohttp
|
|||
|
|
|
|||
|
|
login_api = "https://sso.weidian.com/user/login"
|
|||
|
|
headers = {
|
|||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|||
|
|
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) "
|
|||
|
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
|||
|
|
"Referer": "https://sso.weidian.com/login/index.php",
|
|||
|
|
"Origin": "https://sso.weidian.com",
|
|||
|
|
}
|
|||
|
|
payload = {"phone": phone, "password": password, "loginMode": "password"}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
async with aiohttp.ClientSession() as session:
|
|||
|
|
async with session.post(login_api, data=payload, headers=headers) as resp:
|
|||
|
|
data = await resp.json()
|
|||
|
|
status_code = data.get("status", {}).get("status_code", -1)
|
|||
|
|
status_reason = data.get("status", {}).get("status_reason", "未知错误")
|
|||
|
|
|
|||
|
|
if status_code == 0:
|
|||
|
|
api_cookies = data.get("result", {}).get("cookie", [])
|
|||
|
|
pw_cookies = []
|
|||
|
|
for c in api_cookies:
|
|||
|
|
pw_cookies.append({
|
|||
|
|
"name": c.get("name", ""),
|
|||
|
|
"value": c.get("value", ""),
|
|||
|
|
"domain": c.get("domain", ".weidian.com"),
|
|||
|
|
"path": c.get("path", "/"),
|
|||
|
|
"expires": -1,
|
|||
|
|
"httpOnly": c.get("httpOnly", False),
|
|||
|
|
"secure": c.get("secure", False),
|
|||
|
|
"sameSite": "Lax",
|
|||
|
|
})
|
|||
|
|
state = {"cookies": pw_cookies, "origins": []}
|
|||
|
|
auth_path = get_auth_path(account_id)
|
|||
|
|
with open(auth_path, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(state, f, ensure_ascii=False, indent=2)
|
|||
|
|
return True, "API登录成功"
|
|||
|
|
else:
|
|||
|
|
return False, f"API登录失败: {status_reason}"
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"API登录出错: {e}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def get_browser_context(playwright_instance, account_id, headless=True):
|
|||
|
|
"""创建带有已保存登录状态的浏览器上下文"""
|
|||
|
|
browser = await playwright_instance.chromium.launch(
|
|||
|
|
headless=headless, args=['--disable-gpu', '--no-sandbox']
|
|||
|
|
)
|
|||
|
|
device = playwright_instance.devices['iPhone 13']
|
|||
|
|
auth_path = get_auth_path(account_id)
|
|||
|
|
|
|||
|
|
if has_auth(account_id):
|
|||
|
|
context = await browser.new_context(**device, storage_state=auth_path)
|
|||
|
|
else:
|
|||
|
|
context = await browser.new_context(**device)
|
|||
|
|
|
|||
|
|
return browser, context
|