14 KiB
后端登录注册接口说明
本文档描述 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):
app.include_router(auth_router) # 无前缀叠加,router 自身 prefix="/auth"
因此完整 URL 为:
http://<host>:8000/auth/register
http://<host>:8000/auth/login
经 Next.js 代理后(开发环境)也可访问:
http://localhost:3000/auth/register
http://localhost:3000/auth/login
3. 认证流程
3.1 注册 / 登录发 Token
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
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
要点:
- Token 不仅校验 JWT 签名和过期时间,还必须在
user_sessions表中有对应且未吊销的记录。 - 数据库存的是 Token 的 SHA256 哈希,不是明文 Token。
- 每个 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):
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 - Salt:16 字节随机 hex(
secrets.token_hex(16))
6.2 注册时
password_hash=hash_password(payload.password)
6.3 登录时
verify_password(payload.password, user.password_hash)
校验失败统一返回 401 Invalid account or password,不区分「账号不存在」与「密码错误」。
7. Access Token 结构
Token 由 create_access_token() 生成,格式为三段式 Base64URL:
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
7.1 Header
{"alg": "HS256", "typ": "JWT"}
7.2 Payload
{
"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 签名
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 |
业务逻辑:
account、email去空格 / 小写处理- 查询
sys_users,若account或email已存在 →409 Conflict - 创建用户并
flush - 调用
_issue_token()写入user_sessions并返回 Token
成功响应: 201 Created,Body 为 TokenResponse
curl 示例:
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 |
业务逻辑:
- 按
account查用户 verify_password失败 →401 Unauthorized- 成功则
_issue_token()并返回
成功响应: 200 OK,Body 为 TokenResponse
curl 示例:
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
{
"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 统一处理:
{
"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
async def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
session: AsyncSession = Depends(get_db_session),
) -> User:
...
9.1 客户端如何携带 Token
Authorization: Bearer <access_token>
9.2 校验步骤
- 检查
Authorization头是否存在且 scheme 为bearer _decode_access_token():验证格式、HMAC 签名、exp未过期- 从 payload 读取
sub(user_id)、jti - 数据库查询:
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()
- 查不到用户 →
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 内部流程
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 ... |
登录成功后前端会:
- 调用
setSession(session)写入 Zustand +localStorage.auth_token - 跳转到
/central-config/user/employee或redirect参数指定页
登出:仅前端 logout() 清除本地存储,不会通知后端吊销会话。
12. 本地调试
12.1 启动后端
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
12.2 Swagger 文档
http://127.0.0.1:8000/docs
在 Swagger 中可找到 auth 分组下的 register、login。
12.3 用 Token 访问受保护接口
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. 部署注意事项
- 必须修改
JWT_SECRET_KEY,不要使用默认值change-me-in-production。 - 首次部署需执行数据库迁移:
uv run alembic upgrade head(包含sys_users、user_sessions建表)。 - Docker / K8s 环境中,容器内
API_BASE_URL=http://127.0.0.1:8000供 Next.js 代理;客户端浏览器仍通过同源/auth/*访问。 - 当前无服务端 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 |