feat: 完善接口

This commit is contained in:
2026-10-01 03:58:51 -07:00
parent 9534fa0ae5
commit fd9d1353c1
8 changed files with 86 additions and 25 deletions
+3
View File
@@ -1,6 +1,9 @@
# 运行环境标识,development、testing 或 production
ENVIRONMENT=development
# 是否开放用户注册接口,关闭后注册请求返回 403
REGISTRATION_ENABLED=true
# MySQL 连接地址,mysql+asyncmy://用户名:密码@主机:端口/数据库名
DATABASE_URL=mysql+asyncmy://center:change-me@127.0.0.1:3306/center
+22 -12
View File
@@ -23,35 +23,45 @@ Python 3.11+ · FastAPI · SQLAlchemy 2.0 · MySQL · Redis · Alembic · uv
uv sync
Copy-Item .env.example .env
uv run alembic upgrade head
uv run fastapi dev app/main.py
uv run fastapi dev app/main.py --port 8100
```
启动前请在 `.env` 中配置数据库连接、Redis 连接及可信回跳来源。`DATABASE_URL`、`REDIS_URL` 和
`REDIS_KEY_PREFIX` 为必填项,缺失时应用会拒绝启动。完整配置和注释见 [`.env.example`](.env.example)。
`REDIS_KEY_PREFIX` 为必填项,缺失时应用会拒绝启动。将 `REGISTRATION_ENABLED` 设置为 `false`
可关闭公开注册接口。完整配置和注释见 [`.env.example`](.env.example)。
服务默认运行在 `http://127.0.0.1:8000`,交互式 API 文档 `http://127.0.0.1:8000/docs`
服务默认运行在 `http://127.0.0.1:8100`,交互式 API 文档 `http://127.0.0.1:8100/docs`
## API
| 方法 | 路径 | 说明 |
|--------|----------------------|--------------------|
| `GET` | `/health` | 检查服务及依赖状态 |
| `POST` | `/api/auth/register` | 注册用户 |
| `POST` | `/api/auth/login` | 登录并签发 Token |
| `POST` | `/api/auth/verify` | 验证 Token |
| `POST` | `/api/auth/logout` | 注销当前 Token |
| `GET` | `/api/users/me` | 获取当前用户信息 |
| 方法 | 路径 | 说明 |
|--------|----------------------|------------------------------|
| `GET` | `/health` | 检查服务、依赖及注册开关状态 |
| `POST` | `/api/auth/register` | 注册用户 |
| `POST` | `/api/auth/login` | 登录并签发 Token |
| `POST` | `/api/auth/verify` | 验证 Token 与回跳地址 |
| `POST` | `/api/auth/logout` | 注销当前 Token |
| `GET` | `/api/users/me` | 获取当前用户信息 |
登录示例:
```bash
curl -X POST http://127.0.0.1:8000/api/auth/login \
curl -X POST http://127.0.0.1:8100/api/auth/login \
-H "Content-Type: application/json" \
-d '{"account":"alice","password":"password123","from_url":"https://app.example.com/callback"}'
```
受保护接口使用 `Authorization: Bearer <token>` 请求头。
已登录回跳需要把待校验地址交给后端,并且只使用响应中的 `data.from_url`:
```bash
curl -X POST http://127.0.0.1:8100/api/auth/verify \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"from_url":"https://app.example.com/callback"}'
```
## 响应协议
所有接口返回统一结构:
+15 -6
View File
@@ -10,9 +10,10 @@ from app.api.dependencies import get_bearer_token, get_current_user
from app.common.response import ApiResponse, success_response
from app.core.config import Settings, get_settings
from app.core.database import get_db
from app.core.exceptions import BizException
from app.core.redis import get_redis
from app.model.user import User
from app.schema.auth import LoginRequest, LoginResponse
from app.schema.auth import LoginRequest, LoginResponse, VerifyRequest, VerifyResponse
from app.schema.user import UserRegisterRequest, UserResponse
from app.service.auth_service import AuthService
from app.service.token_service import TokenService
@@ -25,8 +26,11 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
async def register_user(
request: UserRegisterRequest,
session: Annotated[AsyncSession, Depends(get_db)],
settings: Annotated[Settings, Depends(get_settings)],
) -> ApiResponse[UserResponse]:
"""注册新的中心平台用户"""
if not settings.registration_enabled:
raise BizException(403, "注册已关闭")
user = await UserService(session).register(request)
return success_response(UserResponse.model_validate(user))
@@ -43,12 +47,17 @@ async def login_user(
return success_response(result)
@router.post("/verify", response_model=ApiResponse[UserResponse])
@router.post("/verify", response_model=ApiResponse[VerifyResponse])
async def verify_token(
current_user: Annotated[User, Depends(get_current_user)],
) -> ApiResponse[UserResponse]:
"""验证 Token 并返回当前用户身份"""
return success_response(UserResponse.model_validate(current_user))
request: VerifyRequest,
token: Annotated[str, Depends(get_bearer_token)],
session: Annotated[AsyncSession, Depends(get_db)],
redis_client: Annotated[Redis, Depends(get_redis)],
settings: Annotated[Settings, Depends(get_settings)],
) -> ApiResponse[VerifyResponse]:
"""验证 Token 和回跳地址并返回当前用户身份"""
result = await AuthService(session, redis_client, settings).verify(token, request)
return success_response(result)
@router.post("/logout", response_model=ApiResponse[dict[str, object]])
+16 -3
View File
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.auth import router as auth_router
from app.api.user import router as user_router
from app.common.response import ApiResponse, success_response
from app.core.config import Settings, get_settings
from app.core.database import get_db
from app.core.redis import get_redis
@@ -18,12 +19,24 @@ api_router.include_router(auth_router)
api_router.include_router(user_router)
@api_router.get("/health", response_model=ApiResponse[dict[str, str]], tags=["系统"])
@api_router.get(
"/health",
response_model=ApiResponse[dict[str, str | bool]],
tags=["系统"],
)
async def health_check(
session: Annotated[AsyncSession, Depends(get_db)],
redis_client: Annotated[Redis, Depends(get_redis)],
) -> ApiResponse[dict[str, str]]:
settings: Annotated[Settings, Depends(get_settings)],
) -> ApiResponse[dict[str, str | bool]]:
"""检查应用及外部依赖的健康状态"""
await session.execute(text("SELECT 1"))
await redis_client.ping()
return success_response({"status": "ok", "database": "ok", "redis": "ok"})
return success_response(
{
"status": "ok",
"database": "ok",
"redis": "ok",
"registration_enabled": settings.registration_enabled,
}
)
+1
View File
@@ -13,6 +13,7 @@ def _split_origins(value: str) -> list[str]:
class Settings(BaseSettings):
environment: str = "development"
registration_enabled: bool = True
database_url: str = Field(min_length=1)
redis_url: str = Field(min_length=1)
redis_key_prefix: str = Field(
+2 -2
View File
@@ -6,9 +6,9 @@ from fastapi import FastAPI
ERROR_RESPONSES: dict[str, tuple[int, ...]] = {
"/health": (500,),
"/api/auth/register": (400, 500),
"/api/auth/register": (400, 403, 500),
"/api/auth/login": (400, 401, 500),
"/api/auth/verify": (401, 403, 500),
"/api/auth/verify": (400, 401, 403, 500),
"/api/auth/logout": (401, 403, 500),
"/api/users/me": (401, 403, 500),
}
+11 -1
View File
@@ -2,6 +2,8 @@
from pydantic import BaseModel, Field
from app.schema.user import UserResponse
class LoginRequest(BaseModel):
account: str = Field(min_length=1, max_length=255)
@@ -11,6 +13,14 @@ class LoginRequest(BaseModel):
class LoginResponse(BaseModel):
token: str
token_type: str = "Bearer"
expires_in: int
from_url: str | None = None
class VerifyRequest(BaseModel):
from_url: str = Field(min_length=1, max_length=2048)
class VerifyResponse(BaseModel):
user: UserResponse
from_url: str
+16 -1
View File
@@ -8,7 +8,8 @@ from app.core.exceptions import BizException
from app.core.security import validate_from_url, verify_password
from app.model.user import User
from app.repository.user_repository import UserRepository
from app.schema.auth import LoginRequest, LoginResponse
from app.schema.auth import LoginRequest, LoginResponse, VerifyRequest, VerifyResponse
from app.schema.user import UserResponse
from app.service.token_service import TokenService
@@ -73,3 +74,17 @@ class AuthService:
await self.tokens.delete(token)
raise BizException(403)
return user
async def verify(self, token: str, request: VerifyRequest) -> VerifyResponse:
"""验证 Token 后校验并返回可信回跳地址"""
user = await self.authenticate(token)
from_url = validate_from_url(
request.from_url,
self.settings.trusted_redirect_origin_list,
)
if from_url is None:
raise BizException(400, "参数错误")
return VerifyResponse(
user=UserResponse.model_validate(user),
from_url=from_url,
)