Files
brapi-java/docs/dev/backend/auth.md
2026-05-28 11:56:17 +08:00

534 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 后端登录注册接口说明
本文档描述 `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` |