fix:sample/plate 之前的开发

This commit is contained in:
彭帅
2026-05-28 11:56:17 +08:00
parent fc36bc83e3
commit 8b65de36b8
367 changed files with 57752 additions and 947 deletions

533
docs/dev/backend/auth.md Normal file
View File

@@ -0,0 +1,533 @@
# 后端登录注册接口说明
本文档描述 `common-br-api` 项目中用户认证注册、登录、Token 校验)的后端实现,便于前后端联调与部署排查。
---
## 1. 总览
| 项目 | 说明 |
|------|------|
| 路由前缀 | `/auth` |
| 框架 | FastAPI |
| 用户表 | `sys_users` |
| 会话表 | `user_sessions` |
| 密码算法 | PBKDF2-HMAC-SHA256210000 次迭代) |
| Token 形式 | 自实现 JWT 风格 Bearer TokenHS256 签名) |
| 默认有效期 | 24 小时1440 分钟) |
**当前已实现的 HTTP 接口:**
| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/auth/register` | 注册并自动登录 |
| `POST` | `/auth/login` | 账号密码登录 |
**尚未实现的接口(前端 UI 可能已预留):**
- `/auth/logout`(登出 / 服务端吊销会话)
- `/auth/refresh`(刷新 Token
- 手机号 / 短信验证码登录
---
## 2. 代码结构
```
app/
├── api/
│ └── auth.py # 注册、登录、get_current_user 依赖
├── core/
│ └── security.py # 密码哈希、Token 签发、Token 哈希
├── models/
│ └── auth.py # User、UserSession ORM 模型
├── config/
│ └── settings.py # JWT 密钥、过期时间等配置
└── main.py # 挂载 auth 路由
migrations/versions/
└── 7c91a64f2b2e_create_auth_tables.py # 建表迁移
```
路由挂载(`app/main.py`
```python
app.include_router(auth_router) # 无前缀叠加router 自身 prefix="/auth"
```
因此完整 URL 为:
```text
http://<host>:8000/auth/register
http://<host>:8000/auth/login
```
经 Next.js 代理后(开发环境)也可访问:
```text
http://localhost:3000/auth/register
http://localhost:3000/auth/login
```
---
## 3. 认证流程
### 3.1 注册 / 登录发 Token
```mermaid
sequenceDiagram
participant Client as 客户端
participant API as /auth/*
participant DB as PostgreSQL
Client->>API: POST /auth/register 或 /auth/login
API->>DB: 查重 / 校验密码
API->>API: create_access_token(sub, account, jti)
API->>DB: INSERT user_sessions(jti, token_hash, expires_at)
API-->>Client: TokenResponse(access_token, expires_at, user)
```
### 3.2 受保护接口校验 Token
```mermaid
sequenceDiagram
participant Client as 客户端
participant API as 业务接口
participant DB as PostgreSQL
Client->>API: Authorization: Bearer <token>
API->>API: 校验 JWT 签名与 exp
API->>DB: JOIN sys_users + user_sessions<br/>匹配 user_id, jti, token_hash
alt 会话有效
API-->>Client: 200 + 业务数据
else 无效或过期
API-->>Client: 401 / 403
end
```
**要点:**
1. Token 不仅校验 JWT 签名和过期时间,还必须在 `user_sessions` 表中有对应且未吊销的记录。
2. 数据库存的是 Token 的 **SHA256 哈希**,不是明文 Token。
3. 每个 Token 有唯一 `jti`JWT ID用于会话追踪。
---
## 4. 数据库设计
### 4.1 `sys_users`
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | UUID | PK | 用户 ID |
| `account` | VARCHAR(100) | UNIQUE, NOT NULL | 登录账号 |
| `password_hash` | TEXT | NOT NULL | PBKDF2 哈希串 |
| `name` | VARCHAR(100) | NOT NULL | 显示名称 |
| `email` | VARCHAR(255) | UNIQUE, NOT NULL | 邮箱(注册时转小写) |
| `phone` | VARCHAR(50) | NULL | 手机号(可选) |
| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 |
| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 |
索引:
- `ux_users_account`account 唯一)
- `ux_users_email`email 唯一)
### 4.2 `user_sessions`
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | UUID | PK | 会话记录 ID |
| `user_id` | UUID | FK → sys_users.id, CASCADE | 所属用户 |
| `jti` | VARCHAR(64) | UNIQUE, NOT NULL | Token 会话 ID |
| `token_hash` | TEXT | NOT NULL | SHA256(access_token) |
| `expires_at` | TIMESTAMPTZ | NOT NULL | 过期时间 |
| `revoked_at` | TIMESTAMPTZ | NULL | 吊销时间(预留,当前无 logout 接口写入) |
| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 |
| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 |
---
## 5. 环境配置
配置类:`app/config/settings.py`
| 环境变量 | 默认值 | 说明 |
|----------|--------|------|
| `JWT_SECRET_KEY` | `change-me-in-production` | HMAC 签名密钥,**生产必须修改** |
| `JWT_ALGORITHM` | `HS256` | 写入 JWT Header实际签名为 HMAC-SHA256 |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `1440`24h | Token 有效期(分钟) |
| `DATABASE_URL` | 见 `.env.example` | PostgreSQL 连接串 |
示例(`.env`
```env
JWT_SECRET_KEY=your-long-random-secret
DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/brapi-python
```
---
## 6. 密码安全
实现文件:`app/core/security.py`
### 6.1 哈希格式
```
pbkdf2_sha256$210000$<salt_hex>$<digest_hex>
```
- 算法:`PBKDF2-HMAC-SHA256`
- 迭代次数:`210000`
- Salt16 字节随机 hex`secrets.token_hex(16)`
### 6.2 注册时
```python
password_hash=hash_password(payload.password)
```
### 6.3 登录时
```python
verify_password(payload.password, user.password_hash)
```
校验失败统一返回 `401 Invalid account or password`,不区分「账号不存在」与「密码错误」。
---
## 7. Access Token 结构
Token 由 `create_access_token()` 生成,格式为三段式 Base64URL
```text
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
```
### 7.1 Header
```json
{"alg": "HS256", "typ": "JWT"}
```
### 7.2 Payload
```json
{
"sub": "<user_uuid>",
"account": "<login_account>",
"jti": "<32_hex_chars>",
"iat": 1710000000,
"exp": 1710086400
}
```
| Claim | 含义 |
|-------|------|
| `sub` | 用户 UUID 字符串 |
| `account` | 登录账号 |
| `jti` | 会话唯一 ID |
| `iat` | 签发时间Unix 秒) |
| `exp` | 过期时间Unix 秒) |
### 7.3 签名
```python
HMAC-SHA256(key=JWT_SECRET_KEY, msg=f"{header_b64}.{payload_b64}")
```
> 注意:项目使用**自实现**的 JWT 编解码(`app/api/auth.py` + `app/core/security.py`),未依赖 PyJWT 库。
---
## 8. HTTP 接口详情
### 8.1 注册 `POST /auth/register`
**请求体 `RegisterRequest`**
| 字段 | 类型 | 必填 | 校验 |
|------|------|------|------|
| `account` | string | 是 | 长度 3100 |
| `password` | string | 是 | 长度 6128 |
| `name` | string | 是 | 长度 1100 |
| `email` | string | 是 | 长度 3255入库前转小写 |
| `phone` | string | 否 | 最大 50 |
**业务逻辑:**
1. `account``email` 去空格 / 小写处理
2. 查询 `sys_users`,若 `account``email` 已存在 → `409 Conflict`
3. 创建用户并 `flush`
4. 调用 `_issue_token()` 写入 `user_sessions` 并返回 Token
**成功响应:** `201 Created`Body 为 `TokenResponse`
**curl 示例:**
```bash
curl -X POST http://127.0.0.1:8000/auth/register \
-H "Content-Type: application/json" \
-d '{
"account": "demo_user",
"password": "demo123456",
"name": "演示用户",
"email": "demo@example.com",
"phone": "13800138000"
}'
```
---
### 8.2 登录 `POST /auth/login`
**请求体 `LoginRequest`**
| 字段 | 类型 | 必填 | 校验 |
|------|------|------|------|
| `account` | string | 是 | 长度 1100 |
| `password` | string | 是 | 长度 1128 |
**业务逻辑:**
1.`account` 查用户
2. `verify_password` 失败 → `401 Unauthorized`
3. 成功则 `_issue_token()` 并返回
**成功响应:** `200 OK`Body 为 `TokenResponse`
**curl 示例:**
```bash
curl -X POST http://127.0.0.1:8000/auth/login \
-H "Content-Type: application/json" \
-d '{
"account": "demo_user",
"password": "demo123456"
}'
```
---
### 8.3 统一成功响应 `TokenResponse`
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"token_type": "bearer",
"expires_at": "2026-05-27T10:00:00+00:00",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"account": "demo_user",
"name": "演示用户",
"email": "demo@example.com",
"phone": "13800138000"
}
}
```
| 字段 | 说明 |
|------|------|
| `access_token` | Bearer Token 字符串 |
| `token_type` | 固定 `"bearer"` |
| `expires_at` | ISO8601 UTC 过期时间 |
| `user` | 当前用户公开信息(不含密码) |
---
### 8.4 错误响应格式
认证相关错误通过 `ApiError` 抛出,由 `app/main.py` 统一处理:
```json
{
"message": "错误摘要",
"detail": "错误摘要或详细信息"
}
```
| HTTP 状态码 | 场景 | message 示例 |
|-------------|------|----------------|
| `401` | 登录失败 | `Invalid account or password` |
| `401` | 缺少 Token | `Missing bearer token` |
| `401` | Token 无效 | `Invalid token signature` / `Session invalid or expired` |
| `403` | Token 过期 | `Token expired` |
| `409` | 注册冲突 | `Account or email already exists` |
| `422` | 参数校验失败 | `Validation failed`detail 为字段错误列表) |
---
## 9. 受保护接口:`get_current_user`
定义位置:`app/api/auth.py`
```python
async def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
session: AsyncSession = Depends(get_db_session),
) -> User:
...
```
### 9.1 客户端如何携带 Token
```http
Authorization: Bearer <access_token>
```
### 9.2 校验步骤
1. 检查 `Authorization` 头是否存在且 scheme 为 `bearer`
2. `_decode_access_token()`验证格式、HMAC 签名、`exp` 未过期
3. 从 payload 读取 `sub`user_id`jti`
4. 数据库查询:
```sql
SELECT sys_users.*
FROM sys_users
JOIN user_sessions ON user_sessions.user_id = sys_users.id
WHERE sys_users.id = :user_id
AND user_sessions.jti = :jti
AND user_sessions.token_hash = sha256(:token)
AND user_sessions.revoked_at IS NULL
AND user_sessions.expires_at > now()
```
5. 查不到用户 → `401 Session invalid or expired`
### 9.3 哪些接口使用了 `get_current_user`
#### `/auth` 模块
register/login 本身不需要 Token
#### `/api/dictionaries` 模块(`app/api/dictionaries.py`
| 接口 | 是否需要登录 |
|------|-------------|
| `GET /api/dictionaries/crops` | 否 |
| `POST /api/dictionaries/crops` | **是** |
| `GET /api/dictionaries/persons` | 否 |
| `POST /api/dictionaries/persons` | 否 |
| `POST /api/dictionaries/countries` | **是** |
| `PUT /api/dictionaries/countries/{code}` | **是** |
#### `/brapi/v2` 模块(`app/api/router.py`
- 多数 **GET** 接口:接收 `Authorization` 头但**不强制校验**BrAPI 兼容占位)
- 多数 **POST / PUT / DELETE** 写操作:通过 `Depends(get_current_user)` **强制登录**
写操作成功后会将 `current_user.id` 写入业务表的 `auth_user_id` 字段,用于记录数据创建者。
> 上传接口 `POST /brapi/v2/upload/image` 当前**未**接入 `get_current_user`,前端可选择性携带 Token。
---
## 10. `_issue_token` 内部流程
```python
async def _issue_token(session: AsyncSession, user: User) -> TokenResponse:
jti = uuid4().hex
token, expires_at = create_access_token(
subject=str(user.id),
account=user.account,
jti=jti,
)
session.add(UserSession(
user_id=user.id,
jti=jti,
token_hash=hash_token(token),
expires_at=expires_at,
))
await session.commit()
return TokenResponse(...)
```
每次登录/注册都会**新增一条** `user_sessions` 记录;旧会话不会自动吊销(除非手动清理数据库或未来实现 logout
---
## 11. 前端对接(简要)
| 文件 | 作用 |
|------|------|
| `frontend/src/services/authService.ts` | 封装 `loginWithPassword``registerAccount` |
| `frontend/src/lib/api/sdk.gen.ts` | OpenAPI 生成的 `/auth/login``/auth/register` 客户端 |
| `frontend/src/stores/modules/auth.ts` | Zustand 持久化 Token 与用户信息 |
| `frontend/src/utils/token.ts` | 从 `localStorage` 读取 `auth_token` |
| `frontend/src/lib/client.ts` | 请求拦截器自动附加 `Authorization: Bearer ...` |
登录成功后前端会:
1. 调用 `setSession(session)` 写入 Zustand + `localStorage.auth_token`
2. 跳转到 `/central-config/user/employee``redirect` 参数指定页
登出:仅前端 `logout()` 清除本地存储,**不会**通知后端吊销会话。
---
## 12. 本地调试
### 12.1 启动后端
```bash
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
```
### 12.2 Swagger 文档
```text
http://127.0.0.1:8000/docs
```
在 Swagger 中可找到 `auth` 分组下的 `register``login`
### 12.3 用 Token 访问受保护接口
```bash
TOKEN="<access_token>"
curl http://127.0.0.1:8000/api/dictionaries/crops \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"crop_name": "maize"}'
```
---
## 13. 部署注意事项
1. **必须修改** `JWT_SECRET_KEY`,不要使用默认值 `change-me-in-production`
2. 首次部署需执行数据库迁移:`uv run alembic upgrade head`(包含 `sys_users``user_sessions` 建表)。
3. Docker / K8s 环境中,容器内 `API_BASE_URL=http://127.0.0.1:8000` 供 Next.js 代理;客户端浏览器仍通过同源 `/auth/*` 访问。
4. 当前无服务端 logoutToken 在过期前始终有效(只要 `user_sessions` 记录存在)。如需强制下线,需扩展 logout 接口并设置 `revoked_at`
---
## 14. 后续可扩展项
| 功能 | 建议实现 |
|------|----------|
| 登出 | `POST /auth/logout`,将当前 `jti``revoked_at` 设为 now |
| 刷新 Token | `POST /auth/refresh`,验证旧 Token 后签发新 Token |
| 当前用户 | `GET /auth/me`,返回 `UserResponse` |
| 修改密码 | `POST /auth/change-password` |
| 手机号登录 | 独立短信验证码表 + 新 login 分支 |
| 上传鉴权 | `upload.py` 增加 `Depends(get_current_user)` |
---
## 15. 相关源码索引
| 内容 | 路径 |
|------|------|
| 路由与 DTO | `app/api/auth.py` |
| 密码 / Token 工具 | `app/core/security.py` |
| ORM 模型 | `app/models/auth.py` |
| 配置 | `app/config/settings.py` |
| 建表迁移 | `migrations/versions/7c91a64f2b2e_create_auth_tables.py` |
| 应用入口 | `app/main.py` |
| 前端封装 | `frontend/src/services/authService.ts` |