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

14 KiB
Raw Permalink Blame History

后端登录注册接口说明

本文档描述 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

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

要点:

  1. Token 不仅校验 JWT 签名和过期时间,还必须在 user_sessions 表中有对应且未吊销的记录。
  2. 数据库存的是 Token 的 SHA256 哈希,不是明文 Token。
  3. 每个 Token 有唯一 jtiJWT 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_accountaccount 唯一)
  • ux_users_emailemail 唯一)

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 144024h 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
  • Salt16 字节随机 hexsecrets.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 长度 3100
password string 长度 6128
name string 长度 1100
email string 长度 3255入库前转小写
phone string 最大 50

业务逻辑:

  1. accountemail 去空格 / 小写处理
  2. 查询 sys_users,若 accountemail 已存在 → 409 Conflict
  3. 创建用户并 flush
  4. 调用 _issue_token() 写入 user_sessions 并返回 Token

成功响应: 201 CreatedBody 为 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 长度 1100
password string 长度 1128

业务逻辑:

  1. account 查用户
  2. verify_password 失败 → 401 Unauthorized
  3. 成功则 _issue_token() 并返回

成功响应: 200 OKBody 为 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 faileddetail 为字段错误列表)

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 校验步骤

  1. 检查 Authorization 头是否存在且 scheme 为 bearer
  2. _decode_access_token()验证格式、HMAC 签名、exp 未过期
  3. 从 payload 读取 subuser_idjti
  4. 数据库查询:
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()
  1. 查不到用户 → 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 封装 loginWithPasswordregisterAccount
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/employeeredirect 参数指定页

登出:仅前端 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 分组下的 registerlogin

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. 部署注意事项

  1. 必须修改 JWT_SECRET_KEY,不要使用默认值 change-me-in-production
  2. 首次部署需执行数据库迁移:uv run alembic upgrade head(包含 sys_usersuser_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,将当前 jtirevoked_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