简化H5应用用户授权
This commit is contained in:
@@ -1,87 +1,97 @@
|
||||
import uuid
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from jose import jwt
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import settings
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import WechatLoginRequest, PhoneLoginRequest, TokenResponse
|
||||
from app.middleware.auth import create_access_token
|
||||
from app.services.wechat import code_to_session
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/demo-login", response_model=TokenResponse)
|
||||
async def demo_login():
|
||||
"""Web 体验用的演示登录,每次创建独立的临时用户"""
|
||||
demo_openid = f"demo_{uuid.uuid4().hex[:12]}"
|
||||
user = User(
|
||||
openid=demo_openid,
|
||||
name="体验用户",
|
||||
)
|
||||
await user.insert()
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=True,
|
||||
)
|
||||
DEV_MODE = os.environ.get("DEV_MODE", "").lower() in ("1", "true", "yes")
|
||||
# 固定测试账号 openid(dev 模式下使用)
|
||||
DEV_OPENID = "dev_test_account_001"
|
||||
|
||||
|
||||
@router.post("/wechat-login", response_model=TokenResponse)
|
||||
async def wechat_login(req: WechatLoginRequest):
|
||||
"""微信小程序登录"""
|
||||
try:
|
||||
wechat_data = await code_to_session(req.code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
class ExchangeCodeRequest(BaseModel):
|
||||
code: str
|
||||
mobile: str | None = None
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
token: str
|
||||
user_id: str
|
||||
openid: str
|
||||
mobile: str
|
||||
|
||||
|
||||
def _build_token(user: User) -> str:
|
||||
"""生成自包含 JWT:sub + openid + mobile"""
|
||||
expire = datetime.now(timezone.utc) + timedelta(days=30)
|
||||
payload = {
|
||||
"sub": str(user.id),
|
||||
"openid": user.openid,
|
||||
"mobile": user.phone or "",
|
||||
"exp": expire,
|
||||
}
|
||||
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
@router.post("/exchange-code")
|
||||
async def exchange_code(req: ExchangeCodeRequest) -> TokenResponse:
|
||||
"""小程序登录:code → openid → 创建/查找 User → 返回自包含 JWT"""
|
||||
wechat_data = await code_to_session(req.code)
|
||||
openid = wechat_data["openid"]
|
||||
user = await User.find_one(User.openid == openid)
|
||||
is_new = user is None
|
||||
|
||||
user = await User.find_one(User.openid == openid)
|
||||
if not user:
|
||||
user = User(
|
||||
openid=openid,
|
||||
session_key=wechat_data.get("session_key"),
|
||||
union_id=wechat_data.get("unionid"),
|
||||
phone=req.mobile or None,
|
||||
)
|
||||
await user.insert()
|
||||
else:
|
||||
# 更新 session_key + 手机号
|
||||
user.session_key = wechat_data.get("session_key")
|
||||
if req.mobile:
|
||||
user.phone = req.mobile
|
||||
await user.save()
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
token = _build_token(user)
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=is_new,
|
||||
openid=user.openid,
|
||||
mobile=user.phone or "",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/phone-login", response_model=TokenResponse)
|
||||
async def phone_login(req: PhoneLoginRequest):
|
||||
"""手机号登录(简化版,生产环境需接入短信验证)"""
|
||||
# TODO: 接入真实短信验证服务
|
||||
if req.sms_code != "123456":
|
||||
raise HTTPException(status_code=400, detail="验证码错误")
|
||||
|
||||
user = await User.find_one(User.phone == req.phone)
|
||||
is_new = user is None
|
||||
@router.post("/dev-login")
|
||||
async def dev_login() -> TokenResponse:
|
||||
"""开发环境:使用固定测试账号,不依赖微信"""
|
||||
if not DEV_MODE:
|
||||
raise HTTPException(status_code=403, detail="仅开发环境可用")
|
||||
|
||||
user = await User.find_one(User.openid == DEV_OPENID)
|
||||
if not user:
|
||||
try:
|
||||
user = User(openid=f"phone_{req.phone}", phone=req.phone)
|
||||
await user.insert()
|
||||
except DuplicateKeyError:
|
||||
# 并发竞态:另一个请求已创建该用户,回退查找
|
||||
user = await User.find_one(User.phone == req.phone)
|
||||
is_new = False
|
||||
user = User(
|
||||
openid=DEV_OPENID,
|
||||
name="开发测试用户",
|
||||
phone="13800000000",
|
||||
)
|
||||
await user.insert()
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
token = _build_token(user)
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=is_new,
|
||||
openid=user.openid,
|
||||
mobile=user.phone or "",
|
||||
)
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WechatLoginRequest(BaseModel):
|
||||
code: str # wx.login() 获取的 code
|
||||
|
||||
|
||||
class PhoneLoginRequest(BaseModel):
|
||||
phone: str
|
||||
sms_code: str
|
||||
class ExchangeCodeRequest(BaseModel):
|
||||
code: str
|
||||
mobile: str | None = None
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
token: str
|
||||
user_id: str
|
||||
is_new_user: bool = False
|
||||
openid: str
|
||||
mobile: str
|
||||
|
||||
@@ -64,6 +64,7 @@ init_config() {
|
||||
APP_PORT=${APP_PORT:-${PORT:-8000}}
|
||||
APP_WORKERS=${APP_WORKERS:-1}
|
||||
APP_LOG_LEVEL=${APP_LOG_LEVEL:-"info"}
|
||||
DEV_MODE=${DEV_MODE:-""}
|
||||
|
||||
# PID 文件路径
|
||||
PID_FILE="${APP_DIR}/.${APP_NAME}.pid"
|
||||
@@ -82,6 +83,7 @@ init_config() {
|
||||
log_info "服务器端口: $APP_PORT"
|
||||
log_info "工作进程数: $APP_WORKERS"
|
||||
log_info "日志级别: $APP_LOG_LEVEL"
|
||||
log_info "开发模式: $([ -n \"$DEV_MODE\" ] && echo \"已启用\" || echo \"已禁用\")"
|
||||
log_info "日志文件: $LOG_FILE"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user