Files
meijiaka-zy/python-api/scripts/test_auth.py
T

154 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""
验证双 Token + 单设备踢人功能
==============================
用法:
cd python-api
python -m scripts.test_auth --mobile 13800138000
流程:
1. 发送验证码
2. 设备 A 登录(获取 Token A)
3. 用 Token A 调用 /me 验证 Access Token 有效
4. 用 Token A 的 refresh_token 刷新(验证 Token 轮换)
5. 设备 B 登录(同一手机号,获取 Token B)
6. 验证 Token A 已失效(被踢)
"""
import argparse
import asyncio
import sys
import time
import httpx
# 测试环境 API 地址
BASE_URL = "https://dev.tapi.meijiaka.cn/api/v1"
async def send_code(mobile: str) -> None:
"""发送验证码(开发环境验证码会打印到后端日志)"""
async with httpx.AsyncClient() as client:
r = await client.post(f"{BASE_URL}/auth/send-code", json={"mobile": mobile})
print(f"[1] 发送验证码: {r.status_code} {r.text}")
r.raise_for_status()
async def login(mobile: str, code: str, device_id: str) -> dict:
"""登录,返回 {access_token, refresh_token, user}"""
async with httpx.AsyncClient() as client:
r = await client.post(
f"{BASE_URL}/auth/login",
json={
"mobile": mobile,
"code": code,
"device_id": device_id,
"device_name": f"测试设备-{device_id[-4:]}",
"os_info": "test-script",
"app_version": "0.1.0",
},
)
print(f"[2] 登录 ({device_id}): {r.status_code}")
r.raise_for_status()
return r.json()["data"]
async def call_me(access_token: str) -> dict:
"""用 Access Token 调用 /me"""
async with httpx.AsyncClient() as client:
r = await client.get(
f"{BASE_URL}/auth/me",
headers={"Authorization": f"Bearer {access_token}"},
)
print(f"[3] /me: {r.status_code} {r.text[:200]}")
r.raise_for_status()
return r.json()["data"]
async def refresh_token(refresh_token: str) -> dict:
"""用 Refresh Token 换取新的 Token 对"""
async with httpx.AsyncClient() as client:
r = await client.post(
f"{BASE_URL}/auth/refresh",
json={"refresh_token": refresh_token},
)
print(f"[4] 刷新 Token: {r.status_code}")
r.raise_for_status()
return r.json()["data"]
async def verify_kicked(access_token: str) -> bool:
"""验证旧 Access Token 是否已失效(被踢)"""
async with httpx.AsyncClient() as client:
r = await client.get(
f"{BASE_URL}/auth/me",
headers={"Authorization": f"Bearer {access_token}"},
)
print(f"[6] 旧 Token 调用 /me: {r.status_code} {r.text[:200]}")
return r.status_code == 401
async def main():
parser = argparse.ArgumentParser(description="验证双 Token + 踢人功能")
parser.add_argument("--mobile", required=True, help="手机号")
parser.add_argument("--code", default="123456", help="验证码(默认 123456,需与日志一致)")
args = parser.parse_args()
mobile = args.mobile
code = args.code
print(f"\n{'='*60}")
print(f"测试手机号: {mobile}")
print(f"验证码: {code}")
print(f"{'='*60}\n")
# 1. 发送验证码
await send_code(mobile)
print("⚠️ 请确认后端日志中的验证码与 --code 一致\n")
# 2. 设备 A 登录
device_a = "device-a-test"
token_a = await login(mobile, code, device_a)
print(f" Access Token A: {token_a['access_token'][:40]}...")
print(f" Refresh Token A: {token_a['refresh_token'][:40]}...")
print()
# 3. 用 Token A 调用 /me
me = await call_me(token_a["access_token"])
print(f" 用户信息: {me['nickname']} ({me['mobile']})\n")
# 4. 用 Refresh Token A 刷新(验证 Token 轮换)
token_a2 = await refresh_token(token_a["refresh_token"])
print(f" New Access Token: {token_a2['access_token'][:40]}...")
print(f" New Refresh Token: {token_a2['refresh_token'][:40]}...")
print()
# 5. 设备 B 登录(同一手机号,触发踢人)
device_b = "device-b-test"
token_b = await login(mobile, code, device_b)
print(f" Access Token B: {token_b['access_token'][:40]}...")
print()
# 6. 验证 Token A(旧的)已失效
is_kicked = await verify_kicked(token_a["access_token"])
if is_kicked:
print("✅ 旧 Token A 已失效(被踢)—— 单设备登录生效\n")
else:
print("❌ 旧 Token A 仍然有效 —— 踢人逻辑有问题\n")
# 7. 验证 Token A2(刷新后的)也应该失效(因为设备 B 登录会覆盖设备记录)
is_kicked2 = await verify_kicked(token_a2["access_token"])
if is_kicked2:
print("✅ 刷新后的 Token A2 也已失效\n")
else:
print("❌ 刷新后的 Token A2 仍然有效\n")
print(f"{'='*60}")
print("验证完成")
print(f"{'='*60}")
if __name__ == "__main__":
asyncio.run(main())