提供基本前后端骨架

This commit is contained in:
hisatri
2026-01-06 23:49:23 +08:00
parent 84d4ccc226
commit 06f8176e23
89 changed files with 19293 additions and 2 deletions

303
docs/architecture.md Normal file
View File

@@ -0,0 +1,303 @@
# 认证系统架构设计文档
## 技术栈
| 组件 | 技术选型 | 说明 |
|------|----------|------|
| Web 框架 | FastAPI 0.128+ | 异步、高性能、自动 OpenAPI 文档 |
| ORM | SQLAlchemy 2.0+ | 异步支持、类型安全 |
| 数据库 | SQLite (aiosqlite) | 开发环境,可切换 PostgreSQL |
| 密码哈希 | Argon2 | 密码学家推荐的哈希算法 |
| 令牌 | PyJWT | JSON Web Token 实现 |
| 数据验证 | Pydantic v2 | 高性能数据验证 |
| 配置管理 | pydantic-settings | 类型安全的环境变量配置 |
## 项目结构
```
app/
├── __init__.py
├── main.py # FastAPI 应用入口
├── database.py # 数据库连接与会话管理
├── core/ # 核心功能模块
│ ├── config.py # 配置管理
│ ├── security.py # 安全功能密码哈希、JWT
│ └── exceptions.py # 自定义异常类
├── models/ # SQLAlchemy ORM 模型
│ └── user.py # 用户数据模型
├── schemas/ # Pydantic 数据模式
│ ├── base.py # 基础响应格式
│ ├── user.py # 用户相关 Schema
│ └── auth.py # 认证相关 Schema
├── repositories/ # 数据访问层 (Repository Pattern)
│ ├── base.py # 基础 CRUD 操作
│ └── user.py # 用户数据仓库
├── services/ # 业务逻辑层 (Service Layer)
│ ├── user.py # 用户服务
│ └── auth.py # 认证服务
└── api/ # API 路由层
├── deps.py # 依赖注入定义
└── v1/
├── router.py # 路由聚合
└── endpoints/
├── auth.py # 认证接口
└── users.py # 用户接口
```
## 分层架构
```
┌─────────────────────────────────────────────┐
│ API Layer │
│ (FastAPI Endpoints + Deps) │
├─────────────────────────────────────────────┤
│ Service Layer │
│ (Business Logic + Rules) │
├─────────────────────────────────────────────┤
│ Repository Layer │
│ (Data Access + Queries) │
├─────────────────────────────────────────────┤
│ Model Layer │
│ (SQLAlchemy ORM Models) │
├─────────────────────────────────────────────┤
│ Database │
│ (SQLite / PostgreSQL) │
└─────────────────────────────────────────────┘
```
### 各层职责
| 层 | 职责 | 示例 |
|----|------|------|
| **API Layer** | 处理 HTTP 请求/响应、参数验证、依赖注入 | `auth.py`, `users.py` |
| **Service Layer** | 业务逻辑、规则校验、跨仓库协调 | `AuthService`, `UserService` |
| **Repository Layer** | 数据库操作封装、查询构建 | `UserRepository` |
| **Model Layer** | 数据结构定义、表关系映射 | `User` |
## 数据流
### 用户注册流程
```
Client Request
┌─────────────────┐
│ API Endpoint │ POST /api/v1/auth/register
│ (auth.py) │ 接收请求、验证 Schema
└────────┬────────┘
│ UserCreate
┌─────────────────┐
│ UserService │ 检查用户名/邮箱唯一性
│ (user.py) │ 哈希密码
└────────┬────────┘
┌─────────────────┐
│ UserRepository │ 创建用户记录
│ (user.py) │ 提交事务
└────────┬────────┘
┌─────────────────┐
│ Database │ INSERT INTO users
└─────────────────┘
```
### 用户登录流程
```
Client Request
┌─────────────────┐
│ API Endpoint │ POST /api/v1/auth/login
│ (auth.py) │ 接收凭证
└────────┬────────┘
│ LoginRequest
┌─────────────────┐
│ AuthService │ 1. 查找用户
│ (auth.py) │ 2. 验证密码
│ │ 3. 检查用户状态
│ │ 4. 更新登录时间
│ │ 5. 生成 JWT
└────────┬────────┘
┌─────────────────┐
│ UserRepository │ 查询用户
│ (user.py) │ 更新 last_login_at
└────────┬────────┘
TokenResponse
```
### 认证流程 (受保护接口)
```
Client Request + Bearer Token
┌─────────────────┐
│ HTTPBearer │ 提取 Authorization header
│ (FastAPI) │
└────────┬────────┘
┌─────────────────┐
│ get_current_user│ 1. 解码 JWT
│ (deps.py) │ 2. 验证签名和过期
│ │ 3. 查询用户
│ │ 4. 检查用户状态
└────────┬────────┘
│ User
┌─────────────────┐
│ API Endpoint │ 使用已认证用户执行业务逻辑
└─────────────────┘
```
## 安全设计
### 密码存储
使用 **Argon2id** 算法(密码哈希竞赛获胜算法):
```python
# 参数配置
time_cost=3 # 迭代次数
memory_cost=65536 # 内存使用 (64MB)
parallelism=4 # 并行度
```
特性:
- 抗 GPU/ASIC 攻击
- 可调参数适应硬件升级
- 自动包含盐值
### JWT 令牌
**Access Token:**
- 算法HS256
- 有效期30 分钟(可配置)
- 包含:用户 ID、用户名、角色
**Refresh Token:**
- 算法HS256
- 有效期7 天(可配置)
- 仅包含:用户 ID
### 防护措施
| 威胁 | 防护 |
|------|------|
| 暴力破解 | Argon2 计算成本 |
| 时序攻击 | 无论用户是否存在都执行密码验证 |
| Token 泄露 | 短期 Access Token + 长期 Refresh Token |
| SQL 注入 | SQLAlchemy 参数化查询 |
| XSS | JSON 响应(非 HTML |
## 扩展点
### 邀请码注册
```python
# schemas/user.py
class UserCreate(UserBase):
password: str
invite_code: str | None = None # 新增字段
# services/user.py
async def create_user(self, user_data: UserCreate) -> User:
if settings.require_invite_code:
await self._validate_invite_code(user_data.invite_code)
# ... 原有逻辑
```
### OAuth2 登录
```python
# services/oauth.py
class OAuthService:
async def authenticate_google(self, code: str) -> User:
...
async def authenticate_github(self, code: str) -> User:
...
# api/v1/endpoints/oauth.py
@router.get("/google/callback")
async def google_callback(code: str, oauth: OAuthService = Depends()):
user = await oauth.authenticate_google(code)
tokens = auth_service.create_tokens(user)
return tokens
```
### 令牌黑名单
```python
# models/token_blacklist.py
class TokenBlacklist(Base):
__tablename__ = "token_blacklist"
jti: Mapped[str] = mapped_column(primary_key=True)
expires_at: Mapped[datetime]
# services/auth.py
async def logout(self, token: str) -> None:
payload = decode_token(token)
await self.blacklist_repo.add(payload["jti"], payload["exp"])
```
## 配置参考
```bash
# .env 配置示例
# 环境
ENVIRONMENT=development # development | staging | production
DEBUG=true
# 安全
SECRET_KEY=your-256-bit-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
# 数据库
DATABASE_URL=sqlite+aiosqlite:///./satonano.db
# PostgreSQL: postgresql+asyncpg://user:pass@localhost/dbname
# 密码策略
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_DIGIT=true
PASSWORD_REQUIRE_SPECIAL=false
```
## 测试建议
```bash
# 安装测试依赖
uv add --dev pytest pytest-asyncio httpx
# 运行测试
uv run pytest tests/ -v
```
测试覆盖点:
- 用户注册(正常、重复用户名、弱密码)
- 用户登录(正常、错误密码、禁用用户)
- 令牌刷新(正常、过期令牌、无效令牌)
- 密码修改(正常、错误当前密码)
- 权限验证(未认证、已认证、管理员)

704
docs/auth-api.md Normal file
View File

@@ -0,0 +1,704 @@
# 用户认证系统 API 文档
## 概述
本文档描述 SatoNano 云服务综合管理平台的用户认证系统 API。
### 基础信息
- **Base URL**: `http://localhost:8000/api/v1`
- **认证方式**: Bearer Token (JWT)
- **Content-Type**: `application/json`
- **支持登录方式**: 用户名密码 / OAuth2 (Linux.do)
### 统一响应格式
所有 API 响应遵循统一格式:
```json
{
"success": true,
"message": "操作成功",
"data": { ... }
}
```
错误响应:
```json
{
"success": false,
"message": "错误描述",
"code": "ERROR_CODE",
"details": { ... }
}
```
---
## 认证接口
### 1. 用户注册
创建新用户账户。
**请求**
```
POST /auth/register
```
**请求体**
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| username | string | ✅ | 用户名3-32位字母开头只允许字母、数字、下划线 |
| password | string | ✅ | 密码8-128位需包含大小写字母和数字 |
| email | string | ❌ | 邮箱地址 |
| nickname | string | ❌ | 昵称最长64位 |
**示例请求**
```json
{
"username": "john_doe",
"password": "SecurePass123",
"email": "john@example.com",
"nickname": "John"
}
```
**成功响应** `201 Created`
```json
{
"success": true,
"message": "注册成功",
"data": {
"id": "875cd9b4-3504-455d-9290-b6d1ba6b56e0",
"username": "john_doe",
"email": "john@example.com",
"nickname": "John",
"avatar_url": null,
"bio": null,
"is_active": true,
"created_at": "2026-01-05T13:39:07.653138",
"last_login_at": null
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 409 | 用户名或邮箱已被注册 |
| 422 | 请求数据验证失败 |
---
### 2. 用户登录
使用用户名/邮箱和密码登录,获取访问令牌。
**请求**
```
POST /auth/login
```
**请求体**
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| username | string | ✅ | 用户名或邮箱 |
| password | string | ✅ | 密码 |
**示例请求**
```json
{
"username": "john_doe",
"password": "SecurePass123"
}
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "登录成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 1800
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 401 | 用户名或密码错误 |
| 403 | 账户已被禁用 |
---
### 3. 刷新令牌
使用刷新令牌获取新的访问令牌。
**请求**
```
POST /auth/refresh
```
**请求体**
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| refresh_token | string | ✅ | 刷新令牌 |
**示例请求**
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "刷新成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 1800
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 401 | 令牌无效或已过期 |
| 403 | 账户已被禁用 |
---
### 4. 用户退出
退出登录。客户端应删除本地存储的令牌。
**请求**
```
POST /auth/logout
```
**请求头**
```
Authorization: Bearer <access_token>
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "退出成功",
"data": null
}
```
---
### 5. 修改密码
修改当前用户的密码。
**请求**
```
POST /auth/change-password
```
**请求头**
```
Authorization: Bearer <access_token>
```
**请求体**
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| current_password | string | ✅ | 当前密码 |
| new_password | string | ✅ | 新密码8-128位 |
**示例请求**
```json
{
"current_password": "SecurePass123",
"new_password": "NewSecurePass456"
}
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "密码修改成功",
"data": null
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 400 | 当前密码错误 / 新密码不符合要求 |
| 401 | 未认证 |
---
## OAuth2 接口
SatoNano 支持通过 Linux.do 平台进行 OAuth2 第三方登录。
### 特性
- **主备端点自动切换**:当首选 OAuth2 端点不可达时,自动回退到备用端点
- **状态码验证**:使用 state 参数防止 CSRF 攻击
- **自动用户创建**:首次登录自动创建本地用户账户
### 1. 获取授权 URL
获取 OAuth2 授权页面 URL用于重定向用户到第三方平台。
**请求**
```
GET /auth/oauth2/authorize
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "请重定向到授权 URL",
"data": {
"authorize_url": "https://connect.linux.do/oauth2/authorize?client_id=xxx&redirect_uri=xxx&response_type=code&state=xxx&scope=read",
"state": "random-state-string"
}
}
```
**使用流程**
1. 前端调用此接口获取 `authorize_url`
2. 将用户重定向到 `authorize_url`
3. 用户在 Linux.do 完成授权
4. Linux.do 重定向回应用的回调 URL
---
### 2. OAuth2 回调
处理 OAuth2 授权回调,完成登录流程。
**请求**
```
GET /auth/oauth2/callback?code=xxx&state=xxx
```
**查询参数**
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| code | string | ✅ | OAuth2 授权码 |
| state | string | ✅ | 状态码(防 CSRF |
**成功响应** `200 OK`
```json
{
"success": true,
"message": "登录成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 1800,
"is_new_user": false
}
}
```
**响应字段说明**
| 字段 | 说明 |
|------|------|
| is_new_user | `true` 表示首次登录,已自动创建账户 |
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 400 | 无效的状态码(可能是 CSRF 攻击或状态已过期) |
| 401 | OAuth2 认证失败(授权码无效等) |
| 503 | OAuth2 未配置或服务不可用 |
---
### OAuth2 登录流程图
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 前端应用 │ │ SatoNano │ │ Linux.do │
└─────┬───────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. GET /auth/oauth2/authorize │
│─────────────────────────> │
│ │ │
│ 2. 返回 authorize_url + state │
│<───────────────────────── │
│ │ │
│ 3. 重定向用户到 authorize_url │
│──────────────────────────────────────────────────>
│ │ │
│ │ 4. 用户授权 │
│ │<───────────────────────│
│ │ │
│ 5. 重定向回 callback?code=xxx&state=xxx │
│<──────────────────────────────────────────────────
│ │ │
│ 6. GET /auth/oauth2/callback │
│─────────────────────────> │
│ │ │
│ │ 7. 用 code 换取 token │
│ │───────────────────────>│
│ │ │
│ │ 8. 返回 access_token │
│ │<───────────────────────│
│ │ │
│ │ 9. 获取用户信息 │
│ │───────────────────────>│
│ │ │
│ │ 10. 返回 user info │
│ │<───────────────────────│
│ │ │
│ 11. 返回 JWT 令牌 │ │
│<───────────────────────── │
│ │ │
```
---
## 用户接口
### 1. 获取当前用户信息
获取当前登录用户的详细信息。
**请求**
```
GET /users/me
```
**请求头**
```
Authorization: Bearer <access_token>
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "操作成功",
"data": {
"id": "875cd9b4-3504-455d-9290-b6d1ba6b56e0",
"username": "john_doe",
"email": "john@example.com",
"nickname": "John",
"avatar_url": null,
"bio": null,
"is_active": true,
"created_at": "2026-01-05T13:39:07.653138",
"last_login_at": "2026-01-05T13:39:19.028376"
}
}
```
---
### 2. 更新当前用户信息
更新当前登录用户的资料。
**请求**
```
PATCH /users/me
```
**请求头**
```
Authorization: Bearer <access_token>
```
**请求体**(所有字段可选)
| 字段 | 类型 | 描述 |
|------|------|------|
| nickname | string | 昵称最长64位 |
| email | string | 邮箱地址 |
| avatar_url | string | 头像 URL最长512位 |
| bio | string | 个人简介最长500位 |
**示例请求**
```json
{
"nickname": "Johnny",
"bio": "Hello, World!"
}
```
**成功响应** `200 OK`
```json
{
"success": true,
"message": "更新成功",
"data": {
"id": "875cd9b4-3504-455d-9290-b6d1ba6b56e0",
"username": "john_doe",
"email": "john@example.com",
"nickname": "Johnny",
"avatar_url": null,
"bio": "Hello, World!",
"is_active": true,
"created_at": "2026-01-05T13:39:07.653138",
"last_login_at": "2026-01-05T13:39:19.028376"
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 409 | 邮箱已被其他用户使用 |
---
### 3. 获取指定用户信息
获取指定用户的信息。
**请求**
```
GET /users/{user_id}
```
**请求头**
```
Authorization: Bearer <access_token>
```
**路径参数**
| 参数 | 类型 | 描述 |
|------|------|------|
| user_id | string | 用户 UUID |
**成功响应** `200 OK`
```json
{
"success": true,
"message": "操作成功",
"data": {
"id": "875cd9b4-3504-455d-9290-b6d1ba6b56e0",
"username": "john_doe",
"email": "john@example.com",
"nickname": "Johnny",
"avatar_url": null,
"bio": "Hello, World!",
"is_active": true,
"created_at": "2026-01-05T13:39:07.653138",
"last_login_at": "2026-01-05T13:39:19.028376"
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 404 | 用户不存在 |
---
## 错误码说明
| 错误码 | HTTP 状态 | 说明 |
|--------|-----------|------|
| AUTHENTICATION_ERROR | 401 | 认证失败 |
| INVALID_CREDENTIALS | 401 | 用户名或密码错误 |
| TOKEN_ERROR | 401 | 令牌无效 |
| TOKEN_EXPIRED | 401 | 令牌已过期 |
| OAUTH2_ENDPOINT_ERROR | 401 | OAuth2 服务不可用 |
| OAUTH2_STATE_ERROR | 400 | OAuth2 状态码无效 |
| OAUTH2_TOKEN_ERROR | 401 | OAuth2 令牌获取失败 |
| OAUTH2_USERINFO_ERROR | 401 | OAuth2 用户信息获取失败 |
| AUTHORIZATION_ERROR | 403 | 权限不足 |
| USER_DISABLED | 403 | 账户已被禁用 |
| RESOURCE_NOT_FOUND | 404 | 资源不存在 |
| USER_ALREADY_EXISTS | 409 | 用户已存在 |
| VALIDATION_ERROR | 422 | 数据验证失败 |
| PASSWORD_VALIDATION_ERROR | 422 | 密码不符合要求 |
---
## 密码策略
默认密码要求:
- 长度8-128 位
- 必须包含至少一个大写字母
- 必须包含至少一个小写字母
- 必须包含至少一个数字
可通过环境变量配置:
```bash
PASSWORD_MIN_LENGTH=8
PASSWORD_MAX_LENGTH=128
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_DIGIT=true
PASSWORD_REQUIRE_SPECIAL=false
```
---
## 令牌说明
### Access Token
- 用于 API 认证
- 默认有效期30 分钟
- 通过 `Authorization: Bearer <token>` 请求头传递
### Refresh Token
- 用于获取新的 Access Token
- 默认有效期7 天
- 仅用于 `/auth/refresh` 接口
### JWT Payload 结构
**Access Token:**
```json
{
"sub": "user-uuid",
"iat": 1767620359,
"exp": 1767622159,
"type": "access",
"username": "john_doe",
"is_superuser": false,
"oauth_provider": null
}
```
> OAuth2 登录的用户 `oauth_provider` 字段为 `"linuxdo"`
**Refresh Token:**
```json
{
"sub": "user-uuid",
"iat": 1767620359,
"exp": 1768225159,
"type": "refresh"
}
```
---
## OAuth2 配置说明
### Linux.do OAuth2 端点
| 端点 | URL |
|------|-----|
| authorize | `https://connect.linux.do/oauth2/authorize` |
| token | `https://connect.linux.do/oauth2/token` |
| userinfo | `https://connect.linux.do/api/user` |
### 在 `config.yaml` 中配置
```yaml
# OAuth2 配置 (Linux.do)
oauth2_client_id: your_client_id
oauth2_client_secret: your_client_secret
oauth2_callback_path: /api/v1/auth/oauth2/callback
# 首选端点
oauth2_authorize_endpoint: https://connect.linux.do/oauth2/authorize
oauth2_token_endpoint: https://connect.linux.do/oauth2/token
oauth2_user_info_endpoint: https://connect.linux.do/api/user
# 备用端点(首选不可达时自动回退)
oauth2_authorize_endpoint_reserve: https://connect.linuxdo.org/oauth2/authorize
oauth2_token_endpoint_reserve: https://connect.linuxdo.org/oauth2/token
oauth2_user_info_endpoint_reserve: https://connect.linuxdo.org/api/user
# 请求超时(秒)
oauth2_request_timeout: 10
```
### Linux.do 返回的用户信息
```json
{
"id": 1,
"username": "neo",
"name": "Neo",
"active": true,
"trust_level": 4,
"email": "u1@linux.do",
"avatar_url": "https://linux.do/xxxx",
"silenced": false
}
```
### 主备端点切换逻辑
1. 首先尝试首选端点(`connect.linux.do`
2. 如果首选端点超时、连接失败或返回 5xx 错误,自动切换到备用端点(`connect.linuxdo.org`
3. 如果备用端点也失败,抛出 `OAUTH2_ENDPOINT_ERROR` 错误