# 后端登录注册接口说明 本文档描述 `common-br-api` 项目中用户认证(注册、登录、Token 校验)的后端实现,便于前后端联调与部署排查。 --- ## 1. 总览 | 项目 | 说明 | |------|------| | 路由前缀 | `/auth` | | 框架 | FastAPI | | 用户表 | `sys_users` | | 会话表 | `user_sessions` | | 密码算法 | PBKDF2-HMAC-SHA256(210000 次迭代) | | Token 形式 | 自实现 JWT 风格 Bearer Token(HS256 签名) | | 默认有效期 | 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://:8000/auth/register http://: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 API->>API: 校验 JWT 签名与 exp API->>DB: JOIN sys_users + user_sessions
匹配 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$$ ``` - 算法:`PBKDF2-HMAC-SHA256` - 迭代次数:`210000` - Salt:16 字节随机 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 .. ``` ### 7.1 Header ```json {"alg": "HS256", "typ": "JWT"} ``` ### 7.2 Payload ```json { "sub": "", "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 | 是 | 长度 3–100 | | `password` | string | 是 | 长度 6–128 | | `name` | string | 是 | 长度 1–100 | | `email` | string | 是 | 长度 3–255,入库前转小写 | | `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 | 是 | 长度 1–100 | | `password` | string | 是 | 长度 1–128 | **业务逻辑:** 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 ``` ### 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="" 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. 当前无服务端 logout:Token 在过期前始终有效(只要 `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` |