本文档按当前代码实现整理,覆盖 app/http/router/internal/admin/auth/auth.go 中 27 个路由的完整功能、调用链路和数据结构。
app/http/router/handler.go:注册/{apiPrefix}app/http/router/internal/handler.go:注册/internalapp/http/router/internal/admin/handler.go:注册/admin(SaveOperationRecord挂载在/admin/system)app/http/router/internal/admin/auth/handler.go:注册/auth
其中 apiPrefix 表示 system.route_prefix;默认值为 dudu-admin-api。
Auth 模块业务接口完整前缀:/{apiPrefix}/internal/admin/auth
| 功能 | 方法 | 路径 | 是否经过 CheckAdminAuth |
|---|---|---|---|
| 获取 OAuth 登录地址 | GET | /oauth/url |
否 |
| 换取登录 Token | POST | /token |
否 |
| 获取 Passkey 登录验证请求 | POST | /passkey/login/options |
否 |
| 完成 Passkey 登录 | POST | /passkey/login/finish |
否 |
| 本地账号重认证 | POST | /reauth |
否 |
| 查询敏感操作验证方式 | GET | /reauth/methods |
是 |
| 密码验证敏感操作 | POST | /reauth/password |
是 |
| TOTP 验证敏感操作 | POST | /reauth/totp |
是 |
| 获取 Passkey 敏感操作验证请求 | POST | /reauth/passkey/options |
是 |
| 完成 Passkey 敏感操作验证 | POST | /reauth/passkey/finish |
是 |
| 确认第三方绑定 | POST | /oauth/bind/confirm |
否 |
| 查询已绑定第三方 | GET | /oauth/accounts |
是 |
| 解绑第三方 | POST | /oauth/unbind |
是 |
| 获取 Passkey 注册验证请求 | POST | /passkey/register/options |
是 |
| 完成 Passkey 注册 | POST | /passkey/register/finish |
是 |
| 查询当前用户 Passkey | GET | /passkeys |
是 |
| 删除当前用户 Passkey | DELETE | /passkey |
是 |
| 获取当前用户信息 | GET | /profile |
是 |
| 重置密码(安全码) | PUT | /password/reset |
否 |
| 修改密码 | PUT | /password |
是 |
| 更新个人资料 | PUT | /profile |
是 |
| 获取用户菜单 | GET | /menus |
是 |
| 修改登录标识 | PUT | /identifier |
是 |
| 开启 TFA | PUT | /tfa/enable |
是 |
| 关闭 TFA | PUT | /tfa/disable |
是 |
| 获取 TOTP Key | GET | /tfa/key |
是 |
| 获取 TFA 状态 | GET | /tfa/status |
是 |
{
"code": 0,
"msg": "ok",
"trace": {
"id": "afeade2f5957-tcdtjo-gdmaj",
"desc": ""
},
"data": {}
}sequenceDiagram
participant C as Client
participant RL as AdminAuthRateLimit
participant MA as CheckAdminAuth
participant H as AdminAuth Handler
participant S as AuthService
participant R as Repository/Redis
C->>RL: 请求 /{apiPrefix}/internal/admin/auth/*
RL->>MA: 进入鉴权中间件(仅受保护接口)
MA->>S: VerifyToken + HasRole/HasPermission
MA-->>H: 通过后写入 user_id/user_name
H->>S: 业务方法
S->>R: DB/Redis/外部OAuth
R-->>S: 返回数据
S-->>H: errCode + data
H-->>C: 统一 JSON 响应
/oauth/url、/token、/passkey/login/options、/passkey/login/finish、/reauth、/oauth/bind/confirm、/password/reset 跳过 CheckAdminAuth。
这些公开接口仍按路由配置受限流控制。
来自 app/http/middleware/check_admin_auth.go:
- 先读 Header:
Authorization: Bearer <token>。 - Header 无 token 时再读 Cookie:
admin-token=<token>。 - 调用
AuthService.VerifyToken做 JWT 校验。 - 成功后写入上下文:
user_id、user_name。
JWT 使用 HS256,过期时间来自 config.System.Admin.TokenExpireIn(若 admin 配置为空则回退到系统配置)。
- 先调用
HasRole(userID, "super_admin")。 - 是
super_admin:直接放行。 - 否则计算权限 hash:
MD5(HTTP_METHOD + RequestPathWithoutQuery)。 - 调用
HasPermission(userID, permissionHash)。
| Code | 含义 | 触发位置 |
|---|---|---|
| 10001 | 未登录/未携带 token | 中间件默认值(未拿到 token) |
| 11005 | 用户授权已过期 | jwt.ErrTokenExpired |
| 11007 | 标识结构异常 | jwt.ErrTokenMalformed |
| 11009 | Token 签名无效 | jwt.ErrTokenSignatureInvalid |
| 11006 | 用户授权失败 | 其他 token 校验失败 |
| 11008 | 用户权限不足 | 非 super_admin 且无接口权限 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| identifier | string | 否 | password 模式传邮箱/手机号;totp 模式传 safe_code |
| grant_type | string | 是 | password / totp / feishu / wechat |
| state | string | 否 | OAuth 回调 state(feishu/wechat 场景使用) |
| credentials | string | 是 | password 模式必须传 md5(明文密码);totp 模式传验证码;OAuth 模式传 code |
| 字段 | 类型 | 说明 |
|---|---|---|
| safe_code | string | 在 NeedTfa(11028) / NeedResetPWD(11015) 返回 |
| token | string | 登录成功返回 JWT |
| expires_in | int64 | token 过期秒数 |
| bind_ticket | string | NeedBindOAuth(11042) 时返回,用于后续绑定确认 |
| oauth_profile | object | NeedBindOAuth(11042) 时返回的第三方资料预览(当前支持 user_name、avatar) |
| syncable_fields | array | NeedBindOAuth(11042) 时返回的可同步字段列表(当前可能包含 user_name、avatar) |
| 字段 | 类型 | 说明 |
|---|---|---|
| safe_code | string | 第一阶段密码校验通过但仍需 TFA 时返回,action=high_risk_reauth |
| reauth_ticket | string | 重认证完成后返回,用于后续绑定/解绑/删除 Passkey 等高风险操作 |
| 字段 | 类型 | 说明 |
|---|---|---|
| challenge_id | string | 服务端生成的验证请求标识,finish 阶段必须原样回传 |
| options | object | WebAuthn 外层 options 对象;登录场景可直接作为 navigator.credentials.get(options) 的参数,注册场景可直接作为 navigator.credentials.create(options) 的参数 |
说明:
- 登录场景返回
CredentialRequestOptions兼容结构,其中options.publicKey为PublicKeyCredentialRequestOptions。 - 注册场景返回
CredentialCreationOptions兼容结构,其中options.publicKey为PublicKeyCredentialCreationOptions。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | uint | Passkey 主键 |
| display_name | string | 设备展示名称 |
| aaguid | string | Authenticator AAGUID,十六进制字符串;可能为空 |
| transports | array | 浏览器上报的传输方式,如 internal、hybrid |
| last_used_at | string/null | 最近一次成功登录时间 |
| created_at | string | 创建时间 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 浏览器返回的 credential id |
| raw_id | string | 否 | 原始 rawId;若为空会回退到 id |
| type | string | 否 | 默认 public-key |
| response | object | 是 | 浏览器返回的 WebAuthn response 结构 |
说明:
response内支持浏览器常见 camelCase 键名,也兼容 snake_case 键名。- 服务端会将其转换为 WebAuthn 协议对象后再校验。
- Redis key:
admin:system:auth:safeCode:{code} - value(JSON,按不同 action 携带不同字段):
{ "user_id": 1, "action": "tfa" } action可为:tfa、reset_password、high_risk_reauthhigh_risk_reauth用途:本地账号Reauth第一阶段密码校验通过后,若该用户已开启 TFA,则返回一次性safe_code供第二阶段提交totp_code。- TTL:
config.System.Admin.SafeCodeExpireIn - 一次性消费:
parseSafeCode读取后删除
bind_ticket- key:
admin:system:auth:bindTicket:{code} - value:
provider/provider_tenant/provider_subject/oauth_profile - 用途:第三方身份已验真,但尚未与本地账号建立绑定
- key:
reauth_ticket- key:
admin:system:auth:reauthTicket:{code} - value:
user_id + action=high_risk_reauth - 用途:高风险操作前确认本地账号所有权
- key:
- 两类 ticket 都不会在读取时立即消费;
bind_ticket会在绑定成功且登录态签发完成后删除,reauth_ticket会在绑定/解绑/删除当前用户 Passkey 成功后删除。
- Redis key:
admin:system:auth:oauth:{state} - value:
feishu或wechat - TTL:180 秒
- 在
POST /token的 OAuth 分支成功读取后删除
- Redis key:
admin:system:auth:passkey:challenge:{challenge_id} - value(JSON):
{ "action": "login", "challenge_id": "RANDOM_CHALLENGE_ID", "session_data": {}, "created_at": 1738838400 } action可为:login、registeruser_id/display_name仅注册阶段会写入;无账号 Passkey 登录不会预先绑定用户。- TTL:
config.System.Admin.WebAuthn.ChallengeExpireIn,默认 180 秒 POST /passkey/*/finish成功或失败返回前都会尝试消费验证请求;验证请求丢失或超时返回11050,内容不匹配返回11051
来自 system.Menu:
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 菜单 ID(来自 gorm.Model) |
| name | string | 菜单名称 |
| path | string | 菜单路径 |
| permission_id | uint | 关联权限 ID |
| parent_id | uint | 父菜单 ID(0 表示根) |
| icon | string | 图标 |
| sort | int | 排序 |
| children | array | 子菜单(递归同结构) |
说明:结构体嵌入 gorm.Model,序列化时还可能包含 CreatedAt/UpdatedAt/DeletedAt 字段。
GET /tfa/key返回:totp_key:32 位 Base32 随机串qr_code:data:image/png;base64,...
- TOTP 校验参数:6 位验证码,30 秒步长,窗口
±1个时间片。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | uint | 用户 ID |
| user_name | string | 用户名 |
| avatar | string | 头像 URL |
| string | 脱敏后的邮箱展示值,不返回明文 | |
| phone | string | 脱敏后的手机号展示值,不返回明文 |
| role_name | string | 角色名称:超级管理员 / 管理员 / 普通用户 |
role_name 判定规则:
- 包含
super_admin:超级管理员 - 仅包含
base:普通用户 - 在
base基础上有其他角色(且不包含super_admin):管理员
| 字段 | 类型 | 说明 |
|---|---|---|
| rp_id | string | Relying Party ID,Passkey 功能必填;通常填写前端域名或其父域名,不带协议、路径、端口 |
| rp_display_name | string | Relying Party 展示名;为空时默认 Dudu Admin |
| rp_origins | array | 允许发起 WebAuthn 的前端 origin 列表,Passkey 功能必填;每项都必须是完整 origin |
| challenge_expire_in | int | Passkey 验证请求有效期,单位秒;默认 180 |
| user_verification | string | 用户验证策略;支持 required / preferred / discouraged,默认 preferred |
说明:
- 若
rp_id或rp_origins未配置,Passkey 注册/登录接口会返回500。 - 当前实现使用 discoverable Passkey 登录,注册时会要求 resident key;因此
rp_id会直接决定凭证作用域与后续子域共享边界。 challenge_expire_in同时影响 Redis 中验证请求的 TTL,以及 WebAuthn 注册/登录超时时间。challenge_expire_in <= 0时会回退到180;user_verification为空或不是合法值时会按preferred处理。- 示例配置可参考
bin/configs/dev.json、bin/configs/local.json.default、bin/configs/prod.json。
rp_id 表示 Passkey 所属的 Relying Party 域范围,是最重要的 WebAuthn 配置项之一。
- 应填写域名或主机名,例如
localhost、127.0.0.1、admin.example.com、example.com。 - 不要填写
https://admin.example.com、admin.example.com:3000、/login这类带协议、端口或路径的值。 - 当前页面若运行在
https://a.b.com,通常可选a.b.com或b.com;两者都可能合法,但作用域不同:a.b.com:作用域更窄,只面向当前子域。b.com:作用域更宽,适合明确需要多个子域共享 Passkey 的场景。
- 一旦生产环境已签发 Passkey,再修改
rp_id,旧凭证通常需要重新注册。
rp_display_name 是站点展示名,主要用于浏览器或系统弹窗里给用户显示“这是谁在请求 Passkey”。
- 建议填写稳定的产品名,例如
Dudu Admin、Acme Admin。 - 为空时后端会回退为
Dudu Admin。 - 该字段不是凭证存储提供方名称,不会决定用户看到的是
iCloud Keychain、Apple Passwords、Google Password Manager还是Bitwarden。 iCloud Keychain、Bitwarden这类信息通常表示凭证管理器或存储位置,由浏览器、系统和密码管理器决定,不是本项目自动推导出来的。
rp_origins 表示允许发起 WebAuthn 的前端来源列表;这里填的是前端页面 origin,不是后端 API 地址。
- 每项都必须是完整 origin,包含协议、主机和可选端口,例如:
http://localhost:3000http://127.0.0.1:3000https://admin.example.com
- 不要写成
/admin、admin.example.com、https://admin.example.com/login、https://api.example.com这类不匹配当前前端页面来源的值。 - 如果管理端存在多个合法入口,需要将所有入口都加入数组。
rp_id决定凭证归属范围,rp_origins决定哪些页面允许发起 WebAuthn;两者必须同时正确。
challenge_expire_in 表示一次 Passkey 验证请求从生成到失效的时间窗口,单位为秒。
- 默认值为
180。 - 当前实现会把该值同时用于:
- Redis 中
passkey challenge的 TTL; - WebAuthn 注册超时;
- WebAuthn 登录超时。
- Redis 中
- 建议取值:
- 本地开发:
180 - 日常生产:
120到300
- 本地开发:
- 值过小容易导致用户尚未完成系统弹窗就已过期;值过大则会增加请求被重复利用的时间窗口。
user_verification 控制认证器是否要求用户在本地完成“本人验证”,例如指纹、人脸或设备 PIN。
required:必须完成本地验证,安全性最高,兼容性相对更严格。preferred:尽量完成本地验证;当前项目默认值,也是大多数后台系统更平衡的选择。discouraged:尽量不强制本地验证,通常不建议用于高敏感后台。- 如果配置为空或写成其他非法值,当前实现会按
preferred处理,不会抛出单独的配置错误。
本地开发:
"webauthn": {
"rp_id": "localhost",
"rp_display_name": "Dudu Admin",
"rp_origins": ["http://localhost:3000"],
"challenge_expire_in": 180,
"user_verification": "preferred"
}单域生产:
"webauthn": {
"rp_id": "admin.example.com",
"rp_display_name": "Dudu Admin",
"rp_origins": ["https://admin.example.com"],
"challenge_expire_in": 180,
"user_verification": "preferred"
}多个子域共享 Passkey:
"webauthn": {
"rp_id": "example.com",
"rp_display_name": "Dudu Admin",
"rp_origins": [
"https://admin.example.com",
"https://console.example.com"
],
"challenge_expire_in": 180,
"user_verification": "preferred"
}最后一种写法只适合你明确要在多个子域共享同一套 Passkey 体系的场景;如果只是单后台域名,优先使用更窄的 rp_id。
/passkey/register/finish与/passkey/login/finish不记录原始操作载荷。- 请求日志会对
challenge_id、credential、attestation、assertion、public_key、signature、user_handle等敏感字段做脱敏。
- 校验
identifier/credentials非空(否则11000)。 - 校验
identifier为邮箱或手机号(否则11007)。 - 查询用户:
identifier(email/phone) + status=1(不存在11002)。 - 校验密码:使用 bcrypt 校验前端传入的
credentials(失败返回11001)。 - 若用户已开启 TFA =>
11028并返回safe_code(action=tfa)。 - 其余情况生成 JWT 并返回
token + expires_in。
- 参数映射:
identifier=safe_code,credentials=totp_code。 - 校验
safe_code并要求action=tfa(失败11030)。 - 校验用户 TOTP(失败
11029)。 - 成功返回 JWT。
- 校验
safe_code/password非空(11034/11032)。 - 解析
safe_code且action=reset_password(否则11030)。 - 更新密码:以 bcrypt 存储
password(当前接口口径下,password为前端传入的md5(明文密码))。
- 生成 16 位
state,缓存 180 秒。 - 按
type构造重定向 URL:feishuwechat(login_type=qrcode时走企业微信扫码地址)
- 校验
state与 oauthType 匹配(否则11041)。 - 通过对应 provider API 换取用户标识。
- 按
(provider, provider_tenant, provider_subject)查询sys_user_identity。 - 若已绑定用户,直接生成 JWT,并更新该 identity 的
last_login_at。 - 若第三方未绑定用户,返回
11042且携带bind_ticket。 NeedBindOAuth场景会同时返回第三方资料预览与可同步字段列表;前端可让用户勾选本次要同步的字段。
- 传入本地
identifier + password。 - 使用密码验证本地账号所有权。
- 若用户未开启 TFA,直接返回短期
reauth_ticket。 - 若用户已开启 TFA,返回
11028,并携带safe_code(action=high_risk_reauth)。
- 传入第一阶段返回的
safe_code与当前totp_code。 - 校验
safe_code且要求action=high_risk_reauth(失败11030)。 - 查询该
safe_code对应用户,并校验用户仍处于启用状态。 - 校验用户 TOTP(失败
11029)。 - 成功后返回短期
reauth_ticket,仅用于绑定/解绑/删除 Passkey 等高风险操作。
- 前端先调用
GET /reauth/methods获取当前用户的可用验证方式、默认方式和password_requires_totp标记。 - 若当前用户已配置 Passkey,则默认方式为
passkey;用户仍可手动切换到密码链路。 - Passkey 链路:
- 调用
POST /reauth/passkey/options获取challenge_id + options - 浏览器执行
navigator.credentials.get(options) - 调用
POST /reauth/passkey/finish完成校验并换取reauth_ticket
- 调用
- 密码链路:
- 调用
POST /reauth/password提交当前密码 - 若用户未开启 TFA,直接返回
reauth_ticket - 若用户已开启 TFA,返回
11028 + safe_code - 再调用
POST /reauth/totp提交safe_code + totp_code,换取reauth_ticket
- 调用
- 以下敏感操作统一要求先完成上述验证并携带
reauth_ticket:- 修改密码
- 修改登录标识
- 管理第三方账号
- 管理 Passkey
- 管理双因素认证
- 传入
bind_ticket与reauth_ticket。 - 校验两张 ticket 均有效,且
reauth_ticket.action=high_risk_reauth。 - 在事务中校验目标第三方身份未被其他用户占用。
- 写入
sys_user_identity。 - 若传入
sync_fields,同步对应资料到sys_user。 - 绑定成功后回查目标用户并直接签发 JWT,返回
token + expires_in。 - 成功后消费两张 ticket。
- 前端先调用
GET /oauth/url获取第三方授权地址,并跳转到第三方完成授权。 - 第三方回调后,前端携带
code + state调用POST /token(grant_type=feishu/wechat)。 - 若该第三方身份已绑定本地账号,则
POST /token直接返回 JWT,流程结束。 - 若该第三方身份尚未绑定本地账号,则
POST /token返回11042,并携带bind_ticket + oauth_profile + syncable_fields。 - 前端提示用户输入要绑定的本地账号和密码,并调用
POST /reauth第一阶段。 - 若本地账号未开启 TFA,则
POST /reauth直接返回reauth_ticket。 - 若本地账号已开启 TFA,则
POST /reauth返回11028 + safe_code;前端继续提示输入 TOTP,再调用POST /reauth第二阶段换取reauth_ticket。 - 前端携带
bind_ticket + reauth_ticket调用POST /oauth/bind/confirm;如用户勾选了资料同步项,再附带sync_fields。 - 绑定成功后,接口会直接返回 JWT,前端应立即建立登录态,无需再额外调用一次登录接口。
- 此后再次走同一第三方登录时,将直接命中已绑定用户并返回 JWT。
- 前端直接调用
POST /passkey/login/options,无需先提交账号。 - 服务端返回验证请求标识
challenge_id与options。 - 前端将
options作为整个参数对象传给navigator.credentials.get(options),获取浏览器返回的 assertion。 - 前端把
challenge_id + credential提交到POST /passkey/login/finish。 - 服务端根据凭证中的
userHandle与credential id定位用户,完成验证请求校验、WebAuthn 签名校验,并回写sign_count + last_used_at,最终返回token + expires_in。
补充说明:
- 登录阶段
response字段应保持浏览器原样透传,服务端同时支持 camelCase 与 snake_case 键名。 POST /passkey/login/options与POST /passkey/login/finish走管理端鉴权频控。- 当前项目不做历史兼容;切换到无账号 Passkey 登录后,开发阶段已注册的旧 Passkey 需要删除并重新注册。
GET /oauth/accounts:返回当前登录用户已绑定的第三方账号列表,每条记录都包含id,前端解绑时必须使用该标识。POST /oauth/unbind:请求体必须传identity_id + reauth_ticket;服务端只会解绑这条指定 identity,解绑前会校验账号至少保留一种可用登录方式。
- 当前登录用户先完成“已登录用户敏感操作统一二次验证”,再调用
POST /passkey/register/options,并携带reauth_ticket。 - 服务端返回验证请求标识
challenge_id与options,前端将options作为整个参数对象传给navigator.credentials.create(options)。 - 前端把
challenge_id + credential提交到POST /passkey/register/finish,成功后返回新建PasskeyItem。 GET /passkeys可查询当前用户已绑定的全部 Passkey。DELETE /passkey需携带id + reauth_ticket删除单个 Passkey;若删除后将不再保留任何可用登录方式,返回11049。- 若 credential 已存在,注册完成阶段返回
11053;若凭证验证失败则返回11054。
-
Method:GET
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/url -
鉴权:否
-
Query:
字段 类型 必填 说明 type string 是 feishu/wechatlogin_type string 否 wechat可传qrcode -
响应:
data.url(string) -
错误码:
400、11040、500
- Method:POST
- Path:
/{apiPrefix}/internal/admin/auth/token - 鉴权:否
- Body:
AuthParam - 响应:
AccessToken - OAuth 未绑定时返回示例:
{ "code": 11042, "msg": "NeedBindOAuth", "trace": { "id": "afeade2f5957-tcdtjo-gdmaj", "desc": "" }, "data": { "bind_ticket": "BIND_TICKET", "oauth_profile": { "user_name": "张三", "avatar": "https://example.com/avatar.png" }, "syncable_fields": ["user_name", "avatar"] } } - 错误码(按分支):
- 通用:
400、11010 - password:
11000、11001、11002、11015、11028 - totp:
11029、11030、11002 - OAuth:
11041、500
- 通用:
- Method:GET
- Path:
/{apiPrefix}/internal/admin/auth/profile - 鉴权:是
- 响应:
{ "id": 1, "user_name": "admin", "avatar": "https://...", "email": "a***n@e*****e.com", "phone": "+86*******0000", "role_name": "管理员" } - 标识字段说明:
email、phone仅用于前端展示,响应中返回脱敏值,不返回数据库中的明文标识。- 前端如直接基于该响应初始化“修改登录标识”表单,可在未修改某字段时原样回传该脱敏值;服务端会识别为“该字段未变更”。
- role_name 说明:
超级管理员:用户角色中包含super_admin普通用户:用户角色仅包含base管理员:在base基础上存在其他角色(且不包含super_admin)
- 错误码:第 3 章鉴权错误码
- 业务边界说明:按 service 逻辑,用户不存在时应为
11002;但当前 handler 会在err == nil时强制置code=0,因此该场景可能返回code=0, data=null(当前实现行为)。
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/password/reset -
鉴权:否
-
Body:
字段 类型 必填 说明 safe_code string 是 POST /token返回的安全码password string 是 新密码,前端必须传 md5(明文密码)(服务端使用 bcrypt 存储) -
错误码:
400、11032、11034、11030、11002、500
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/password -
鉴权:是
-
Body:
字段 类型 必填 说明 reauth_ticket string 是 通过统一敏感操作验证流程换取的票据 password string 是 新密码,前端必须传 md5(新明文密码)(服务端使用 bcrypt 存储) -
校验规则:
- 必须先完成统一敏感操作验证。
- 有 Passkey 时默认优先使用 Passkey,也可手动切换到密码链路。
- 无 Passkey 且已开启 TFA 时,前端需按“密码 -> TOTP”两阶段换取
reauth_ticket。
-
错误码:
400、11032、11046、11047、11002、500+ 第 3 章鉴权错误码
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/profile -
鉴权:是
-
Body:
字段 类型 必填 说明 user_name string 否 用户名(受保留词规则限制) avatar string 否 头像 URL -
保留词规则:
admin/root/administrator/管理员/超级管理员/seakee/super_admin/superAdmin(前缀或后缀命中均无效) -
错误码:
400、11007、11002、500+ 第 3 章鉴权错误码
- Method:GET
- Path:
/{apiPrefix}/internal/admin/auth/menus - 鉴权:是
- 响应:
data.items(见 4.6 菜单树结构) - 权限逻辑:
super_admin:返回全部菜单树- 普通用户:按角色权限聚合菜单,并自动补齐父级菜单后返回树形结构
- 错误码:当前控制器将 service 异常统一映射为
400,并附带错误信息;另含第 3 章鉴权错误码
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/identifier -
鉴权:是
-
Body:
字段 类型 必填 说明 reauth_ticket string 是 通过统一敏感操作验证流程换取的票据 email string 否 新邮箱(需合法格式) phone string 否 新手机号(需合法格式) -
校验规则:
- 必须先完成统一敏感操作验证。
- 缺失或非法
reauth_ticket返回11046 / 11047。 email、phone为空时表示不提交该标识;两者都为空返回11014。- 若前端直接回传
GET /profile中对应字段的脱敏值,服务端会按“当前字段未修改”处理,并恢复为当前账号的原值后继续做唯一性和格式校验。 - 只有用户实际输入的新邮箱/新手机号需要满足合法格式并通过唯一性校验。
-
错误码:
400、11007、11014、11046、11047、11002、11013、500+ 第 3 章鉴权错误码
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth -
鉴权:否
-
Body:
字段 类型 必填 说明 identifier string 第一阶段必填 本地邮箱/手机号 password string 第一阶段必填 md5(当前明文密码)safe_code string 第二阶段必填 第一阶段返回的安全码,要求 action=high_risk_reauthtotp_code string 第二阶段必填 当前用户 TOTP 验证码 -
调用方式:
- 第一阶段:只传
identifier + password - 第二阶段:只传
safe_code + totp_code
- 第一阶段:只传
-
第一阶段请求示例:
{ "identifier": "admin@example.com", "password": "md5(当前明文密码)" } -
第一阶段需继续 TFA 时响应示例:
{ "safe_code": "SAFE_CODE" } -
第二阶段请求示例:
{ "safe_code": "SAFE_CODE", "totp_code": "123456" } -
成功响应:
{ "reauth_ticket": "REAUTH_TICKET" } -
说明:
- 若第一阶段命中
11028,表示本地账号密码已通过,但还需要继续提交第二阶段 TOTP。 safe_code为一次性凭证,读取校验后即消费,不能重复使用。
- 若第一阶段命中
-
错误码:
400、11000、11001、11002、11028、11030、11033、11029、500
- Method:GET
- Path:
/{apiPrefix}/internal/admin/auth/reauth/methods - 鉴权:是
- 成功响应:
default_method:passkey/passwordavailable_methods:当前可选方式列表password_requires_totp:密码链路是否还需继续输入 TOTPtotp_enabled:当前用户是否已开启 TFApasskey_count:当前用户已注册 Passkey 数量
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/password -
鉴权:是
-
Body:
字段 类型 必填 说明 password string 是 md5(当前明文密码) -
行为说明:
- 若用户未开启 TFA,直接返回
reauth_ticket - 若用户已开启 TFA,返回
11028 + safe_code
- 若用户未开启 TFA,直接返回
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/totp -
鉴权:是
-
Body:
字段 类型 必填 说明 safe_code string 是 POST /reauth/password返回的安全码totp_code string 是 当前用户 TOTP 验证码 -
成功响应:
{ "reauth_ticket": "REAUTH_TICKET" }
- Method:POST
- Path:
/{apiPrefix}/internal/admin/auth/reauth/passkey/options - 鉴权:是
- 响应:
PasskeyOptionsResult
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/passkey/finish -
鉴权:是
-
Body:
字段 类型 必填 说明 challenge_id string 是 POST /reauth/passkey/options返回的验证请求标识credential object 是 浏览器返回的 PasskeyCredential -
成功响应:
{ "reauth_ticket": "REAUTH_TICKET" }
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/bind/confirm -
鉴权:否
-
Body:
字段 类型 必填 说明 bind_ticket string 是 POST /token返回的绑定票据reauth_ticket string 是 POST /reauth返回的重认证票据sync_fields array 否 用户勾选的同步字段列表;当前支持 user_name、avatar -
请求示例:
{ "bind_ticket": "BIND_TICKET", "reauth_ticket": "REAUTH_TICKET", "sync_fields": ["user_name", "avatar"] } -
成功响应:
AccessToken{ "code": 0, "msg": "ok", "trace": { "id": "afeade2f5957-tcdtjo-gdmaj", "desc": "" }, "data": { "token": "JWT_TOKEN", "expires_in": 7200 } } -
行为说明:
- 该接口是“第三方登录未绑定”链路的最后一步。
- 绑定成功后会直接签发登录态,前端无需再额外调用
POST /token。 - 若本次勾选
sync_fields,返回的 JWT 会基于同步后的最新用户资料生成。
-
错误码:
400、11044、11045、11046、11047、11043、11002、500
-
Method:GET
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/accounts -
鉴权:是
-
响应:
data.list[]字段 类型 说明 id uint identity 主键,解绑时必须使用 provider string 第三方类型,如 feishu/wechatprovider_tenant string 第三方租户标识 display_name string 第三方侧展示名称 avatar_url string 第三方侧头像地址 bound_at string 绑定时间 last_login_at string 最近一次通过该身份登录时间 -
错误码:
11002、500+ 第 3 章鉴权错误码
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/unbind -
鉴权:是
-
Body:
字段 类型 必填 说明 identity_id uint 是 GET /oauth/accounts返回的 identity 主键reauth_ticket string 是 POST /reauth返回的重认证票据 -
请求示例:
{ "identity_id": 12, "reauth_ticket": "REAUTH_TICKET" } -
行为说明:
- 服务端按
identity_id精确解绑,不再根据provider/provider_tenant批量删除。 - 解绑会物理删除对应
sys_user_identity记录,后续允许重新绑定同一个第三方身份。 - 若该账号只剩最后一种可用登录方式,则返回
11049。
- 服务端按
-
错误码:
400、11046、11047、11048、11049、11002、500+ 第 3 章鉴权错误码
- Method:POST
- Path:
/{apiPrefix}/internal/admin/auth/passkey/login/options - 鉴权:否
- 响应:
PasskeyOptionsResult - 说明:
data.options为 WebAuthn 登录外层 options 对象,可直接作为navigator.credentials.get(data.options)的参数。- 无需传账号,服务端会生成 discoverable Passkey 登录所需的 options。
- 该接口启用管理端登录频控。
- 错误码:
400、11056、500
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/login/finish -
鉴权:否
-
Body:
字段 类型 必填 说明 challenge_id string 是 POST /passkey/login/options返回的验证请求标识credential object 是 浏览器返回的 PasskeyCredential -
成功响应:
AccessToken -
错误码:
400、11002、11050、11051、11052、11054、500
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/register/options -
鉴权:是
-
Body:
字段 类型 必填 说明 reauth_ticket string 是 通过统一敏感操作验证流程换取的票据 display_name string 否 自定义设备展示名;为空时后端按用户资料自动生成 -
响应:
PasskeyOptionsResult -
说明:
data.options为 WebAuthn 注册外层 options 对象,可直接作为navigator.credentials.create(data.options)的参数。- 该接口会在生成 challenge 成功后消费
reauth_ticket。
-
错误码:
400、11002、11046、11047、11055、500+ 第 3 章鉴权错误码
-
Method:POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/register/finish -
鉴权:是
-
Body:
字段 类型 必填 说明 challenge_id string 是 POST /passkey/register/options返回的验证请求标识credential object 是 浏览器返回的 PasskeyCredential -
成功响应:
PasskeyItem -
错误码:
400、11002、11050、11051、11053、11054、11055、500+ 第 3 章鉴权错误码
-
Method:GET
-
Path:
/{apiPrefix}/internal/admin/auth/passkeys -
鉴权:是
-
响应:
data.list[]字段 类型 说明 id uint Passkey 主键 display_name string 设备展示名 aaguid string Authenticator AAGUID,可能为空 transports array 浏览器上报的传输方式 last_used_at string/null 最近使用时间 created_at string 创建时间 -
错误码:
11002、500+ 第 3 章鉴权错误码
-
Method:DELETE
-
Path:
/{apiPrefix}/internal/admin/auth/passkey -
鉴权:是
-
Body:
字段 类型 必填 说明 id uint 是 要删除的 Passkey 主键 reauth_ticket string 是 POST /reauth返回的重认证票据 -
行为说明:
- 按当前登录用户 +
id精确删除。 - 删除前必须先完成一次高风险重认证,并提交
reauth_ticket。 - 若当前账号只剩最后一种可用登录方式,则返回
11049。 - 缺失或非法
id/reauth_ticket会在 handler 层直接返回400。
- 按当前登录用户 +
-
错误码:
400、11046、11047、11049、11052、500+ 第 3 章鉴权错误码
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/enable -
鉴权:是
-
Body:
字段 类型 必填 说明 reauth_ticket string 是 通过统一敏感操作验证流程换取的票据 totp_code string 是 基于 totp_key生成的验证码totp_key string 是 由 /tfa/key获取 -
错误码:
400、11035、11033、11029、11046、11047、500+ 第 3 章鉴权错误码
-
Method:PUT
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/disable -
鉴权:是
-
Body:
字段 类型 必填 说明 reauth_ticket string 是 通过统一敏感操作验证流程换取的票据 -
错误码:
400、11046、11047、11002、500+ 第 3 章鉴权错误码
-
Method:GET
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/key -
鉴权:是
-
响应:
字段 类型 说明 totp_key string 新生成的 Base32 密钥(长度 32) qr_code string 对应二维码的 base64 data URL -
错误码:
11002(用户不存在) + 第 3 章鉴权错误码
-
Method:GET
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/status -
鉴权:是
-
响应:
字段 类型 说明 enable bool 当前用户是否开启 TFA -
错误码:
11002(用户不存在) + 第 3 章鉴权错误码
当前实现中,密码相关接口仍要求前端先传 md5(明文密码)。服务端会将该摘要使用 bcrypt 存储。
POST /token的grant_type=password:credentials必须传md5(明文密码)。PUT /password/reset:password必须传md5(新明文密码)。- 已登录用户的敏感安全操作统一走“先验证,后提交
reauth_ticket”:- 先调用
GET /reauth/methods获取默认方式; - Passkey 链路:
POST /reauth/passkey/options->POST /reauth/passkey/finish; - 密码链路:
POST /reauth/password,必要时再调用POST /reauth/totp。
- 先调用
PUT /password/PUT /identifier/PUT /tfa/enable/PUT /tfa/disable/POST /passkey/register/options/DELETE /passkey:- 必须携带
reauth_ticket; PUT /password的password仍需传md5(新明文密码)。
- 必须携带
POST /reauth:- 第一阶段传
identifier + password=md5(当前明文密码); - 若返回
11028,第二阶段必须改传safe_code + totp_code; - 不要在第二阶段重复传
identifier/password,统一按safe_code完成 TOTP 校验。
- 第一阶段传
POST /reauth/password:password必须传md5(当前明文密码);- 若返回
11028,继续改传safe_code + totp_code到POST /reauth/totp。
POST /oauth/bind/confirm:- 必须携带
bind_ticket + reauth_ticket; - 若希望首次绑定时同步第三方资料,可传
sync_fields,当前支持user_name、avatar; - 成功后接口会直接返回
token + expires_in,前端应按登录成功处理。
- 必须携带
POST /passkey/login/options:- 无需传账号;
- 旧开发数据里的 Passkey 需要删除后重新注册,才能稳定支持无账号登录。
POST /passkey/register/finish/POST /passkey/login/finish/POST /reauth/passkey/finish:credential必须透传浏览器原始 WebAuthn 结果;rawId/clientDataJSON/attestationObject/authenticatorData/signature/userHandle支持 camelCase 与 snake_case 键名。
POST /passkey/register/options:
- 必须先完成统一敏感操作验证,并携带
reauth_ticket; display_name可选;- 若不传,后端会回退到
user_name或当前登录标识,自动生成xxx Passkey。
DELETE /passkey:
- 必须先完成统一敏感操作验证获取
reauth_ticket; - 请求体必须携带
id + reauth_ticket。
示例(密码链路完成敏感操作验证后修改密码):
{
"reauth_ticket": "REAUTH_TICKET",
"password": "md5(新明文密码)"
}示例(完成统一验证后修改登录标识):
{
"reauth_ticket": "REAUTH_TICKET",
"email": "new_admin@example.com"
}