hyapp-server/docs/auth-login-register-technical-design.md
2026-04-25 13:21:39 +08:00

17 KiB
Raw Blame History

Login And Registration Technical Design

本文档定义 v1 登录、三方注册登录、密码设置和 token 生命周期。它只描述当前架构事实和接口边界,不记录实现流水账。

Scope

v1 只有一个用户创建入口:

  • 三方身份登录即注册。provider + provider_subject 不存在时创建用户、生成 default_display_user_id、设置当前 display_user_id、绑定三方身份并签发 token。

v1 的密码能力只有一个前置条件:

  • 用户必须已经通过三方登录拿到 access token之后才能设置密码。

v1 不支持:

  • 账号密码注册。
  • 用户名、邮箱、手机号作为独立登录账号。
  • 未登录状态设置密码。
  • 手机验证码登录。
  • 匿名游客登录。
  • 多账号主动合并。
  • 客户端直接访问 user-service
  • gateway 保存密码、三方凭证或 refresh token。

Ownership

登录注册归属 user-servicegateway 只做外部 HTTP JSON 入口和 gRPC 协议转换。

Capability Owner Rule
HTTP JSON API gateway-service 只暴露 /api/v1/auth/*,统一返回 envelope
用户创建 user-service 只能由三方身份首次登录触发
展示短号 user-service 新用户创建时生成默认短号,后续登录用当前有效短号;临时靓号有效期内会覆盖默认短号
密码身份 user-service 已登录用户手动设置后才存在
三方身份绑定 user-service provider + subject 唯一绑定用户
access token 签发 user-service 登录成功后签发
access token 校验 gateway-service 业务 API 和设置密码入口校验
refresh token user-service 只保存 hash支持轮换和失效
房间身份态 room-service 只消费已鉴权 actor_user_id,不处理登录
WebSocket 身份态 im-service 校验 access token不处理注册

gateway-service 不读取用户表,不校验密码 hash不调用三方平台做最终身份判断。

External HTTP API

所有接口使用 gateway 业务响应 envelope

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {}
}

request_id 来自 X-Request-ID 或由 gateway 生成,并透传到内部 gRPC meta。

Third Party Login Or Register

POST /api/v1/auth/third-party/login

Request:

{
  "provider": "wechat",
  "credential": "provider_code_or_token",
  "device_id": "device_xxx"
}

Response data:

{
  "user_id": 10001,
  "display_user_id": "100001",
  "default_display_user_id": "100001",
  "display_user_id_kind": "default",
  "display_user_id_expires_at_ms": 0,
  "session_id": "sess_xxx",
  "access_token": "jwt_xxx",
  "refresh_token": "refresh_xxx",
  "expires_in_sec": 1800,
  "token_type": "Bearer",
  "is_new_user": true
}

三方接口不拆注册和登录。新用户不会默认生成密码;只有 usersuser_display_user_idsthird_party_identities 和首个 auth_sessions 会在同一事务中创建。

display_user_id 永远表示当前有效展示号。用户没有临时靓号时,它等于 default_display_user_id;用户有 active 靓号时,它等于靓号。

Set Password

POST /api/v1/auth/password/set
Authorization: Bearer <access_token>

Request:

{
  "password": "plain_password"
}

Response data:

{
  "password_set": true
}

设置密码只接受已登录用户的 access token。客户端不能在 body 中自报 user_iddisplay_user_id。重复设置密码返回 PASSWORD_ALREADY_SET,后续如果要支持改密,必须新增独立改密接口和旧密码校验规则。

Password Login

POST /api/v1/auth/password/login

Request:

{
  "display_user_id": "100001",
  "password": "plain_password",
  "device_id": "device_xxx"
}

Response data:

{
  "user_id": 10001,
  "display_user_id": "100001",
  "default_display_user_id": "100001",
  "display_user_id_kind": "default",
  "display_user_id_expires_at_ms": 0,
  "session_id": "sess_xxx",
  "access_token": "jwt_xxx",
  "refresh_token": "refresh_xxx",
  "expires_in_sec": 1800,
  "token_type": "Bearer"
}

短号不存在、用户未设置密码和密码错误都返回 AUTH_FAILED。这三个分支不能给客户端更细错误,避免登录入口变成用户枚举接口。

密码登录只接受当前有效 display_user_id。如果用户申请了临时靓号,默认短号在靓号有效期内不能用于密码登录;靓号过期后,默认短号恢复登录能力。

Refresh Token

POST /api/v1/auth/token/refresh

Request:

{
  "refresh_token": "refresh_xxx",
  "device_id": "device_xxx"
}

Response data:

{
  "user_id": 10001,
  "display_user_id": "100001",
  "default_display_user_id": "100001",
  "display_user_id_kind": "default",
  "display_user_id_expires_at_ms": 0,
  "session_id": "sess_xxx",
  "access_token": "jwt_xxx",
  "refresh_token": "refresh_new_xxx",
  "expires_in_sec": 1800,
  "token_type": "Bearer"
}

refresh token 必须轮换。旧 refresh token 使用成功后立即失效。

Logout

POST /api/v1/auth/logout

Request:

{
  "refresh_token": "refresh_xxx",
  "session_id": "sess_xxx"
}

Response data:

{
  "revoked": true
}

Internal gRPC Contract

api/proto/user/v1/auth.proto 承载 auth RPC

service AuthService {
  rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse);
  rpc LoginThirdParty(LoginThirdPartyRequest) returns (AuthResponse);
  rpc SetPassword(SetPasswordRequest) returns (SetPasswordResponse);
  rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
  rpc Logout(LogoutRequest) returns (LogoutResponse);
}

关键消息:

message LoginPasswordRequest {
  RequestMeta meta = 1;
  string display_user_id = 2;
  string password = 3;
}

message SetPasswordRequest {
  RequestMeta meta = 1;
  int64 user_id = 2;
  string password = 3;
}

message AuthToken {
  int64 user_id = 1;
  string session_id = 2;
  string access_token = 3;
  string refresh_token = 4;
  int64 expires_in_sec = 5;
  string token_type = 6;
  string display_user_id = 7;
  string default_display_user_id = 8;
  string display_user_id_kind = 9;
  int64 display_user_id_expires_at_ms = 10;
}

SetPasswordRequest.user_id 必须来自 gateway 已校验 access token 后的上下文,不能来自客户端 JSON body。

Data Model

users

用户主记录,只保存用户全局身份和状态。

Column Type Rule
user_id bigint primary key
default_display_user_id varchar user's permanent fallback short id
current_display_user_id varchar current effective short id; pretty id can cover default id
current_display_user_id_kind varchar default or pretty
current_display_user_id_expires_at_ms bigint null for default id
status varchar active, disabled, banned
created_at_ms bigint creation time
updated_at_ms bigint update time

password_accounts

密码身份表。它表达“该用户已经设置过密码”,不表达独立账号体系。

Column Type Rule
user_id bigint primary key
password_hash varchar never store plain password
hash_alg varchar e.g. bcrypt
created_at_ms bigint creation time
updated_at_ms bigint update time

密码登录时先用 users.current_display_user_id 解析当前用户,再按 user_id 读取 password_accounts。这样用户修改默认短号或申请临时靓号后,密码登录自动跟随当前有效短号,不需要同步更新密码表。

如果 current_display_user_id_kind=prettycurrent_display_user_id_expires_at_ms <= now,登录前必须先触发 user-service 的靓号懒过期流程,把当前短号恢复为默认短号后再继续解析。

third_party_identities

三方身份绑定表。

Column Type Rule
id bigint primary key
user_id bigint owner user
provider varchar provider name
provider_subject varchar provider user id, required
provider_union_id varchar optional
created_at_ms bigint creation time
updated_at_ms bigint update time

唯一约束:

unique(provider, provider_subject)

如果 provider 支持 union id也不能只依赖 union id。provider_subject 是登录身份的稳定主键。

auth_sessions

refresh token 和会话状态表。

Column Type Rule
session_id varchar primary key
user_id bigint session owner
refresh_token_hash varchar unique
device_id varchar client device
expires_at_ms bigint refresh expiry
revoked_at_ms bigint null means active
created_at_ms bigint creation time
updated_at_ms bigint update time

refresh token 原文只返回给客户端一次,服务端只保存 hash。

login_audit

登录审计表,不参与主链路判断。

Column Type Rule
id bigint primary key
request_id varchar trace id
user_id bigint nullable on failed login
login_type varchar password, third_party, refresh, set_password
provider varchar nullable
result varchar success, failed
failure_code varchar nullable
client_ip varchar gateway observed ip
user_agent varchar gateway observed ua
created_at_ms bigint event time

审计失败不能影响登录主链路,但不能记录明文密码、三方 credential 或 refresh token 原文。

Core Flows

Third Party Login Or Register

sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service
  participant P as Provider

  C->>G: POST /api/v1/auth/third-party/login
  G->>U: LoginThirdParty(provider, credential)
  U->>P: verify credential
  P-->>U: provider_subject
  U->>U: find identity
  alt identity exists
    U->>U: check user status
    U->>U: create session
  else identity missing
    U->>U: create user + default_display_user_id + third_party identity + session
  end
  U-->>G: AuthToken + is_new_user
  G-->>C: envelope(data=AuthToken)

三方 credential 校验必须在 user-service 内完成。gateway 不保存 provider app secret。

Set Password

sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service

  C->>G: POST /api/v1/auth/password/set + access token
  G->>G: verify access token
  G->>U: SetPassword(user_id from token, password)
  U->>U: check user status
  U->>U: hash password
  U->>U: insert password_accounts by user_id
  U-->>G: password_set=true
  G-->>C: envelope(data={password_set:true})

password_accounts.user_id 是主键。重复设置说明用户已经有密码身份,返回 PASSWORD_ALREADY_SET

Password Login

sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service

  C->>G: POST /api/v1/auth/password/login
  G->>U: LoginPassword(display_user_id, password)
  U->>U: expire pretty display_user_id if needed
  U->>U: resolve current display_user_id
  U->>U: lookup password by user_id
  U->>U: verify password hash
  U->>U: check user status
  U->>U: create session
  U-->>G: AuthToken
  G-->>C: envelope(data=AuthToken)

display_user_id 是登录名,不是另一个账号体系。默认短号变更后旧短号不能继续登录;临时靓号有效期内默认短号不能登录;临时靓号过期后旧靓号不能继续登录。

Refresh Token

sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service

  C->>G: POST /api/v1/auth/token/refresh
  G->>U: RefreshToken
  U->>U: hash refresh token
  U->>U: find active session
  U->>U: revoke old refresh token
  U->>U: create replacement refresh token
  U->>U: expire pretty display_user_id if needed
  U->>U: reload user current display_user_id
  U-->>G: new AuthToken
  G-->>C: envelope(data=AuthToken)

Refresh 返回当前 display_user_id避免客户端持有旧短号。临时靓号已经过期时Refresh 必须返回默认短号。

Token Strategy

v1 使用:

  • access_token: JWT短有效期默认 30 分钟。
  • refresh_token: opaque random token默认 30 天,只保存 hash。
  • session_id: 服务端会话 ID用于 logout、审计和多端管理。

JWT claims

{
  "sub": "10001",
  "user_id": 10001,
  "display_user_id": "100001",
  "default_display_user_id": "100001",
  "display_user_id_kind": "default",
  "display_user_id_expires_at_ms": 0,
  "sid": "sess_xxx",
  "iat": 1777000000,
  "exp": 1777001800,
  "typ": "access"
}

JWT 内的短号字段只是签发时的展示快照,不是身份判断依据。业务鉴权只能用 user_id。如果临时靓号在 access token 有效期内过期,客户端需要通过 Refresh 或用户身份接口拿到恢复后的默认短号。

首版继续沿用本地 HS256 配置。中长期建议迁移到非对称签名:

  • user-service 持私钥签发。
  • gateway-serviceim-service 持公钥校验。
  • 公钥通过配置或 JWKS 缓存分发。

Error Codes

gateway HTTP envelope 的 code 不使用数字。

Code HTTP Meaning
OK 200 success
INVALID_JSON 400 request body is invalid JSON
INVALID_ARGUMENT 400 field format or required field invalid
AUTH_FAILED 401 display_user_id missing, password wrong, no password set, or provider rejected
UNAUTHORIZED 401 missing or invalid access token
PASSWORD_ALREADY_SET 409 user already has password identity
USER_DISABLED 403 user is disabled or banned
SESSION_EXPIRED 401 refresh token expired
SESSION_REVOKED 401 refresh token revoked
UPSTREAM_ERROR 502 user-service unavailable or returned unexpected error
INTERNAL_ERROR 500 gateway internal failure

密码登录时,短号不存在、未设置密码和密码错误都映射为 AUTH_FAILED

Security Rules

  • 明文密码只允许存在于 gateway 入参对象和 user-service hash 前的内存短路径中,不能写日志。
  • password_hash 必须使用强 hash不能使用 SHA、MD5 或可逆加密。
  • refresh token 原文不能入库,只能入库 hash。
  • 三方 provider secret 只属于 user-service 配置。
  • 登录失败日志不能包含明文密码、三方 credential、refresh token 原文。
  • gateway 统一过滤内部错误,客户端只拿稳定错误码和 request_id
  • access token 校验失败不访问 user-service,避免每个业务请求都打用户服务。
  • 设置密码必须从 access token 取 user_id,不接受客户端 body 指定用户。
  • access token 中的 display_user_id 不能作为权限、账务、房间或 IM 身份依据。

Compatibility Rules

当前项目仍在架构阶段,没有外部调用方,本次契约直接删除了旧账号注册接口。后续一旦进入联调或灰度,必须恢复兼容演进纪律:

  • HTTP JSON 字段只能兼容扩展,不能重命名或删除已发布字段。
  • protobuf 字段只能追加,不能复用字段编号。
  • 新增 provider 必须通过配置 allowlist 控制,不能让客户端任意传 provider 后触发未知逻辑。
  • 三方登录创建用户必须使用事务,保证 usersuser_display_user_idsthird_party_identities 不出现半绑定。
  • 设置密码必须使用 user_id 主键幂等判断,不能引入独立账号唯一键。
  • 登录响应可以追加默认短号、当前短号来源和靓号过期时间;已发布后不能改变 display_user_id 表示当前有效短号的语义。

Test Matrix

必须覆盖:

  • 三方身份首次登录创建用户、生成 default_display_user_id,并返回当前 display_user_idis_new_user=true
  • 三方身份再次登录返回同一个 user_idis_new_user=false
  • 三方新用户未设置密码前,短号密码登录返回 AUTH_FAILED
  • 已登录用户设置密码成功。
  • 重复设置密码返回 PASSWORD_ALREADY_SET
  • 设置密码后可使用当前 display_user_id + password 登录。
  • 短号不存在和密码错误都返回 AUTH_FAILED
  • 用户有 active 靓号时,密码登录必须使用靓号,默认短号返回 AUTH_FAILED
  • 靓号过期后,密码登录必须恢复使用默认短号,旧靓号返回 AUTH_FAILED
  • Refresh 在靓号过期后返回默认短号和 display_user_id_kind=default
  • 被禁用用户密码登录返回 USER_DISABLED
  • refresh token 成功轮换,旧 token 再次使用失败。
  • logout 后 refresh token 失败。
  • gateway auth API 响应始终使用 {code,message,request_id,data}
  • gateway 在 SetPassword 中只从 access token 传递 user_id

Open Decisions

这些点需要在实现前定死,不能靠代码猜:

  • 首批三方 provider 名单。
  • 密码复杂度策略。
  • access token 有效期和 refresh token 有效期。
  • HS256 继续使用多久,以及迁移非对称签名的时间点。