25 KiB
萌芽账户认证中心 API 文档
访问 **GET /** 或 **GET /api**(无鉴权)可得到 JSON 格式的简要说明(服务名、版本、links.health、routePrefixes 等)。运行时未挂载返回 Markdown 的 **GET /api/docs;完整 HTTP 清单以 **sproutgate-backend/main.go 与本文件、**sproutgate-backend/后端文档.md** 为准。
接入地址:
- 统一登录前端:
https://auth.shumengya.top - 后端 API:
https://auth.api.shumengya.top - 本地开发 API:
http://<host>:8080
对外接入建议:
- 第三方「登录」按钮跳转到统一登录前端的 授权入口(推荐
.../authorize?...,见下节)。 - 用户在统一登录站点完成登录与授权确认后,回跳到业务站点。
- 业务站点使用回跳 URL
**#** 哈希中的token,由自有后端调用POST /api/auth/verify或GET /api/auth/me校验并建立会话。
推荐入口 URL(生产): 将下列主机换为你的统一登录部署域名;子路径部署时在前端 base 后追加 authorize。
<a href="https://auth.shumengya.top/authorize?redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&state=abc123&client_id=my-app&client_name=%E6%88%91%E7%9A%84%E5%BA%94%E7%94%A8">
使用萌芽统一账户认证登录
</a>
说明:
- 若用户打开的是站点根路径且仅携带
redirect_uri/return_url等查询参数,前端会自动 重定向 到**/authorize**(保留同一套 query),以使用独立授权确认页。 client_id/client_name会由浏览器写入 session,并在用户登录时提交给后端,用于累计 应用接入记录(详见下文「应用接入记录」)。
回跳说明:
- 在
**/authorize** 流程下,已登录用户会看到明确的授权页(应用信息、权限说明、允许/拒绝);拒绝时回跳地址的 hash 中会携带error=access_denied等。 - 登录并「允许」后,浏览器回跳到
redirect_uri(或return_url),并在 URL**#fragment**(哈希)中带上令牌与用户信息(见下表)。 - 第三方拿到
token后,建议服务端调用**POST /api/auth/verify**(不更新「最后访问」等侧写,适合网关鉴权)或**GET /api/auth/me**(会更新访问/签到相关侧写,适合拉取个人中心级资料)校验并解析用户身份。
统一登录前端:查询参数
| 参数 | 必填 | 说明 |
|---|---|---|
redirect_uri |
与 return_url 至少其一 |
登录成功后的回跳地址,须进行 URL 编码;可为绝对 URL 或相对路径(相对路径相对统一登录站点解析)。 |
return_url |
同上 | 与 redirect_uri 同义,二者都传时优先 redirect_uri。 |
state |
否 | OAuth 风格透传字符串;回跳时原样写入哈希参数,供业务防 CSRF 或关联会话。 |
prompt |
否 | 预留;前端可读,当前可用于将来扩展交互策略。 |
client_id |
否 | 第三方应用稳定标识;格式须符合后端校验(见下文「应用接入记录」)。写入 session 后随登录请求提交,用于 应用接入记录。 |
client_name |
否 | 展示用名称(最长 128),与 client_id 配对;可选。 |
回跳 URL:# 哈希参数
成功授权后,前端将使用 [URLSearchParams](https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams) 写入哈希,例如:https://app.example.com/auth/callback#token=...&expiresAt=...&account=...&username=...&state=...。
| 参数 | 说明 |
|---|---|
token |
JWT,调用受保护接口时放在请求头 Authorization: Bearer <token>。 |
expiresAt |
过期时间,RFC3339(与签发侧一致,当前默认为登录时起算 7 天)。 |
account |
账户名(与 JWT sub 一致)。 |
username |
展示用昵称,可能为空。 |
state |
若登录请求携带了 state,则原样返回。 |
业务站点回调页应用脚本读取 location.hash,解析后仅在 HTTPS 环境将 token 存于内存或安全存储,并尽快用后端 **POST /api/auth/verify** 校验(勿仅信任哈希中的明文字段)。
第三方后端接入建议
- 仅信服务端:回调页将
token交给自有后端,由后端请求POST https://<api-host>/api/auth/verify(JSON body:{"token":"..."}),根据valid与user.account建立会话。 - CORS:浏览器直连 API 时须后端已配置 CORS(本服务默认允许任意
Origin);若从服务端发起请求则不受 CORS 限制。 - 令牌无效或封禁:
verify/me返回 401、verify因封禁返回 403(valid: false)时,应作废本地会话并引导用户重新走统一登录。
应用接入记录(authClients)
用户在统一登录前端的 个人中心 → 应用接入 中可看到已与账号发生过认证关联的第三方应用列表。每条记录对应一个稳定的 **client_id,并展示展示名(若有)、首次接入时间、最近接入时间(均为 RFC3339)。持久化在 MySQL users 表中对应用户的 **auth_clients JSON 列;对外 API 字段名仍为 **authClients**(与历史 JSON 文件存储时的形状一致)。
示例展示含义(与界面文案一致):infogenie(万象口袋)首次 2026-03-22T16:33:24+08:00 · 最近 2026-03-30T19:22:45+08:00 —— 其中括号内为 client_name / X-Auth-Client-Name / 登录体 clientName 带来的展示名,infogenie 为 client_id。
client_id 与名称规则
**client_id(必填才能记一条)**:长度 1–64;必须以 英文字母或数字 开头;其余字符仅可为[A-Za-z0-9_.:-](与后端正则一致)。**client_name/ 展示名(可选)**:任意去首尾空白后的字符串,最长 128 字符,超出由服务端截断;对应存储字段为displayName。
在哪些接口上累计记录
记录仅在 令牌有效、用户未封禁、且 client_id 通过校验 时写入或刷新 **lastSeenAt;已存在同一 client_id 时更新 **lastSeenAt,必要时更新 **displayName**,不改动 **firstSeenAt**。
| 方式 | 接口 / 场景 | 如何传递 client_id 与展示名 |
|---|---|---|
| A. 统一登录 URL | 用户经前端完成登录 | 授权页 URL 查询参数 **client_id、**client_name(见上表)。前端写入 session,登录请求 POST /api/auth/login 的 JSON 中会带 **clientId** / **clientName**。 |
| B. 资源拥有者密码登录 | POST /api/auth/login |
JSON **clientId、**clientName(可选,规则同上)。 |
| C. 服务端持 JWT 调用 | POST /api/auth/verify |
请求头 **X-Auth-Client(必须为合法 id)、**X-Auth-Client-Name(可选)。Body 仅含 token,不能替代请求头。 |
| D. 服务端持 JWT 调用 | GET /api/auth/me |
同上:**X-Auth-Client、**X-Auth-Client-Name,与 **Authorization: Bearer** 同时使用。 |
说明:
- A + B:在登录成功、签发 JWT 之后写库;
POST /api/auth/login的响应user为OwnerPublic(),含authClients(若本次写入了新记录)。 - C:
POST /api/auth/verify在校验通过且未封禁后写库;响应user为**Public(),不含authClients**,避免向第三方泄露用户在其他应用上的关联列表。 - D:
GET /api/auth/me在校验通过且未封禁后写库;响应user为**OwnerPublic(),含authClients**,与前端个人中心一致。
封禁用户:POST /api/auth/verify 与 GET /api/auth/me 在命中封禁或令牌无效时 不会 更新应用接入记录(在写库逻辑之前即返回错误)。
浏览器直连 API:若前端或网关需带上 X-Auth-Client,请确认 CORS 允许该自定义头(本服务 CORS 已包含 X-Auth-Client、X-Auth-Client-Name)。
存储结构(authClients 数组元素)
{
"clientId": "infogenie",
"displayName": "万象口袋",
"firstSeenAt": "2026-03-22T16:33:24+08:00",
"lastSeenAt": "2026-03-30T19:22:45+08:00"
}
认证与统一登录
登录获取统一令牌
POST /api/auth/login
请求:
{
"account": "demo",
"password": "demo123",
"clientId": "my-app",
"clientName": "我的应用"
}
clientId / clientName 可选;规则与应用接入请求头一致。传入且格式合法时,会在登录成功后写入 应用接入记录(见上文 「应用接入记录(authClients)」)。
响应:
{
"token": "jwt-token",
"expiresAt": "2026-03-14T12:00:00Z",
"user": {
"account": "demo",
"username": "示例用户",
"email": "demo@example.com",
"level": 0,
"sproutCoins": 10,
"secondaryEmails": ["demo2@example.com"],
"phone": "13800000000",
"avatarUrl": "https://example.com/avatar.png",
"websiteUrl": "https://example.com",
"bio": "### 简介",
"createdAt": "2026-03-14T12:00:00Z",
"updatedAt": "2026-03-14T12:00:00Z"
}
}
若账户已被管理员封禁,返回 403,且不会签发 JWT,响应示例:
{
"error": "account is banned",
"banReason": "违规内容"
}
banReason 可能为空字符串或省略。
常见 HTTP 状态码(登录)
| 状态码 | 含义 |
|---|---|
| 200 | 成功,返回 token、expiresAt、user。 |
| 400 | 请求体非法或缺少 account / password。 |
| 401 | 账户不存在或密码错误(统一文案 invalid credentials)。 |
| 403 | 账户已封禁(见上文 JSON)。 |
| 500 | 服务器内部错误(读库、签发 JWT 失败等)。 |
JWT 概要:算法 HS256;载荷含 account(与 sub 一致)、iss(见 data/config/auth.json)、iat / exp。客户端只需透传字符串,勿在前端解析密钥。
校验令牌
POST /api/auth/verify
请求头:**Content-Type: application/json。可选 **X-Auth-Client、**X-Auth-Client-Name**(累计应用接入记录,见专章)。
请求体:
{
"token": "jwt-token"
}
响应:
{
"valid": true,
"user": { "account": "demo", "...": "..." }
}
若账户已封禁,返回 403,正文示例(不返回 user):
{
"valid": false,
"error": "account is banned",
"banReason": "违规内容"
}
令牌过期、签名错误、issuer 不匹配、token 被撤销(token_epoch)等失败时返回 401,示例:{"valid": false, "error": "invalid token"} 或 {"valid": false, "error": "token revoked"}(以服务端实际字段为准)。
verify 与 me 的取舍:仅校验身份、默认不更新「最后访问」等侧写时用 verify;需要最新资料、签到状态或写入「最后访问」时用 GET /api/auth/me(需 Bearer)。
应用接入记录:在 **verify / me** 上使用 **X-Auth-Client**、**X-Auth-Client-Name** 的约定,以及响应 user 是否包含 authClients,见上文 「应用接入记录(authClients)」。
获取当前用户信息
GET /api/auth/me
请求头:
**Authorization: Bearer <jwt-token>**(必填)
可选 — 应用接入记录(与 verify 相同语义,见 「应用接入记录(authClients)」):
**X-Auth-Client、**X-Auth-Client-Name
可选(由前端调用 https://cf-ip-geo.smyhub.com/api 等接口解析后传入,用于记录「最后访问 IP」与「最后显示位置」):
X-Visit-Ip:客户端公网 IP(与地理接口返回的ip一致即可)X-Visit-Location:展示用位置文案(例如将geo.countryName、regionName、cityName拼接为中国 四川 成都)
服务端回退(避免浏览器跨域导致头缺失):若未传 X-Visit-Location,后端会用 X-Visit-Ip;若也未传 X-Visit-Ip,则用连接的 ClientIP()(请在前置反向代理上正确传递 X-Forwarded-For 等,并在生产环境为 Gin 配置可信代理)。随后服务端请求环境变量 **GEO_LOOKUP_URL** 指定的基址(默认与 internal/clientgeo.DefaultLookupURL 一致:https://cf-ip-geo.smyhub.com/api,实际请求会附加 **?ip=**)解析展示位置并写入用户记录。
响应:
{
"user": { "account": "demo", "...": "..." },
"checkIn": {
"rewardCoins": 1,
"checkedInToday": false,
"lastCheckInDate": "",
"lastCheckInAt": "",
"today": "2026-03-14"
}
}
user还会包含lastVisitAt、lastVisitDate、checkInDays、checkInStreak、visitDays、visitStreak等统计字段。
在登录用户本人、管理员列表等场景下,
user还可包含lastVisitIp、lastVisitDisplayLocation(最近一次通过/api/auth/me上报的访问 IP 与位置文案)。**POST /api/auth/verify返回的user(Public())中不包含这两项**(避免第三方校验响应携带访问隐私)。**GET /api/public/users/:account的user(PublicProfile())会包含这两项**,与公开主页展示策略一致。
说明:密码不会返回。
若账户在登录后被封禁,持旧 JWT 调用 GET /api/auth/me、PUT /api/auth/profile、POST /api/auth/check-in、辅助邮箱等需登录接口时,返回 403,正文同登录封禁响应(error + 可选 banReason)。客户端应作废本地令牌。
每日签到
POST /api/auth/check-in
请求头:
Authorization: Bearer <jwt-token>
响应:
{
"checkedIn": true,
"alreadyCheckedIn": false,
"rewardCoins": 1,
"awardedCoins": 1,
"message": "签到成功",
"user": { "account": "demo", "...": "..." }
}
更新当前用户资料
PUT /api/auth/profile
请求头:
Authorization: Bearer <jwt-token>
请求(字段可选):
{
"password": "newpass",
"username": "新昵称",
"phone": "13800000000",
"avatarUrl": "https://example.com/avatar.png",
"websiteUrl": "https://example.com",
"bio": "### 新简介"
}
说明:websiteUrl 须为 http/https 地址;可传空字符串清除;未写协议时服务端会补全为 https://。
响应:
{
"user": { "account": "demo", "...": "..." }
}
用户广场
获取公开用户目录
GET /api/public/users
无需鉴权。返回未封禁用户的简要列表,默认按 **createdAt**(注册时间)升序。
响应:
{
"total": 2,
"users": [
{
"account": "demo",
"username": "示例用户",
"level": 0,
"sproutCoins": 10,
"avatarUrl": "https://example.com/avatar.png",
"websiteUrl": "https://example.com",
"bio": "### 简介",
"createdAt": "2026-03-14T12:00:00+08:00"
}
]
}
获取用户公开主页
GET /api/public/users/{account}
说明:
- 仅支持路径参数 账户名
account(可与存储大小写不同,服务端按不区分大小写匹配),不支持昵称查询。 - 适合第三方应用展示用户公开资料。
- 若该账户已被封禁,返回 404
{"error":"user not found"}(与不存在账户相同,避免公开资料泄露)。 - 响应体在
user之外包含累计赞数**profileLikeCount**。 **Authorization: Bearer** 可选:若传入有效未封禁用户令牌且非主页主人,响应可额外包含viewerHasLikedToday、viewerLikesRemainingToday、profileLikeDailyMax(当日是否已赞该主页、当日还可给多少人点赞、每自然日上限当前为 5,以后端常量为准)。若浏览者即主页主人,可返回viewerIsOwner: true。user中含该用户最近一次被服务端记录的访问 IP(lastVisitIp)与展示地理位置(lastVisitDisplayLocation);POST /api/auth/verify返回的用户 JSON 不含上述两项。
响应示例(未带 Bearer):
{
"user": {
"account": "demo",
"username": "示例用户",
"level": 3,
"sproutCoins": 10,
"avatarUrl": "https://example.com/avatar.png",
"websiteUrl": "https://example.com",
"lastVisitIp": "203.0.113.1",
"lastVisitDisplayLocation": "中国 广东省 深圳市",
"bio": "### 简介"
},
"profileLikeCount": 12
}
公开主页点赞
POST /api/public/users/{account}/like
请求头:**Authorization: Bearer <jwt-token>**(必填)。
为路径中 **account** 对应用户的公开主页点赞。规则摘要(与 internal/storage 一致):
- 不能给自己点赞;每个点赞者对同一主页 每个自然日最多一次。
- 每个点赞者每个自然日最多给 5 位不同用户点赞(超限返回 400,正文中可含
viewerLikesRemainingToday、profileLikeDailyMax)。 - 目标用户不存在或已封禁:404;点赞者封禁或令牌无效:401 / 403 等,以服务端
error为准。
成功响应示例:
{
"profileLikeCount": 13,
"viewerHasLikedToday": true,
"viewerLikesRemainingToday": 4,
"profileLikeDailyMax": 5
}
公开注册策略
GET /api/public/registration-policy
无需鉴权。用于前端判断是否展示「邀请码」输入框。
响应:
{
"requireInviteCode": false
}
当 requireInviteCode 为 true 时,POST /api/auth/register 必须携带有效 inviteCode(见下节)。
注册账号(发送邮箱验证码)
POST /api/auth/register
请求:
{
"account": "demo",
"password": "demo123",
"username": "示例用户",
"email": "demo@example.com",
"inviteCode": "ABCD1234"
}
inviteCode:可选。若服务端开启「强制邀请码」,则必填且须为管理员发放的未过期、未用尽邀请码。邀请码不区分大小写;成功完成verify-email创建用户后才会扣减使用次数。
响应:
{
"sent": true,
"expiresAt": "2026-03-14T12:10:00Z"
}
验证邮箱并完成注册
POST /api/auth/verify-email
请求:
{
"account": "demo",
"code": "123456"
}
响应:
{
"created": true,
"user": { "account": "demo", "...": "..." }
}
忘记密码(发送重置验证码)
POST /api/auth/forgot-password
请求:
{
"account": "demo",
"email": "demo@example.com"
}
响应:
{
"sent": true,
"expiresAt": "2026-03-14T12:10:00Z"
}
重置密码
POST /api/auth/reset-password
请求:
{
"account": "demo",
"code": "123456",
"newPassword": "newpass"
}
响应:
{ "reset": true }
申请添加辅助邮箱(发送验证码)
POST /api/auth/secondary-email/request
请求头:
Authorization: Bearer <jwt-token>
请求:
{
"email": "demo2@example.com"
}
响应:
{
"sent": true,
"expiresAt": "2026-03-14T12:10:00Z"
}
验证辅助邮箱
POST /api/auth/secondary-email/verify
请求头:
Authorization: Bearer <jwt-token>
请求:
{
"email": "demo2@example.com",
"code": "123456"
}
响应:
{
"verified": true,
"user": { "account": "demo", "...": "..." }
}
管理端接口(需要管理员 Token)
管理员 Token 存放在 data/config/admin.json 中;如果文件不存在,后端启动时会自动生成并写入该文件。
请求时可使用以下任一方式携带:
- Query:
?token=<admin-token> - Header:
X-Admin-Token: <admin-token>
签到奖励设置
GET /api/admin/check-in/config
PUT /api/admin/check-in/config
请求:
{
"rewardCoins": 1
}
- Header:
Authorization: Bearer <admin-token>
注册策略与邀请码
GET /api/admin/registration
响应含 requireInviteCode 与 invites 数组(每项含 code、note、maxUses、uses、expiresAt、createdAt)。maxUses 为 0 表示不限次数。
PUT /api/admin/registration
请求:
{ "requireInviteCode": true }
POST /api/admin/registration/invites
请求:
{
"note": "内测批次",
"maxUses": 10,
"expiresAt": "2026-12-31T15:59:59Z"
}
expiresAt 可省略;须为 RFC3339。响应 201,invite 内含服务端生成的 8 位邀请码。
DELETE /api/admin/registration/invites/{code}
删除指定邀请码(code 与存储大小写可能不同,按不区分大小写匹配)。
获取用户列表
GET /api/admin/users
响应:
{
"total": 1,
"users": [{ "account": "demo", "...": "..." }]
}
新建用户
POST /api/admin/users
请求:
{
"account": "demo",
"password": "demo123",
"username": "示例用户",
"email": "demo@example.com",
"level": 0,
"sproutCoins": 10,
"secondaryEmails": ["demo2@example.com"],
"phone": "13800000000",
"avatarUrl": "https://example.com/avatar.png",
"websiteUrl": "https://example.com",
"bio": "### 简介"
}
更新用户
PUT /api/admin/users/{account}
请求(字段可选):
{
"password": "newpass",
"username": "新昵称",
"level": 1,
"secondaryEmails": ["demo2@example.com"],
"sproutCoins": 99,
"websiteUrl": "https://example.com",
"banned": true,
"banReason": "违规说明(最多 500 字)"
}
banned:是否封禁;解封时请传false,并可将banReason置为空字符串。banReason:仅当用户处于封禁状态时允许设为非空;封禁时若首次写入会记录bannedAt(RFC3339,存于数据库用户记录)。
管理员列表 GET /api/admin/users 中每条 user 为 OwnerPublic(),可含 banned、banReason、bannedAt(及 authClients 等)——以后端实际 JSON 为准。
删除用户
DELETE /api/admin/users/{account}
响应:
{ "deleted": true }
数据存储说明
当前发行版以 MySQL 为唯一运行时数据源(GORM AutoMigrate),不再使用 data/users/*.json 等文件作为线上读写路径。表与用途概要:
| 表名 | 用途 |
|---|---|
users |
用户主数据;auth_clients 等为 JSON 列 |
pending_users |
注册邮箱待验证 |
password_resets |
找回密码验证码 |
secondary_email_verifications |
辅助邮箱验证 |
app_configs |
admin / auth / email / checkin / registration 等 JSON 配置 |
invite_codes |
邀请码 |
profile_likes |
公开主页点赞记录 |
profile_like_daily_quota |
点赞每日额度辅助 |
从旧版 **sproutgate-backend/data/** 目录迁移时,使用 **go run ./cmd/migrate --data-dir ./data**(详见 **sproutgate-backend/后端文档.md**)。
快速联调用示例
# 服务根路径 JSON 说明
curl -s http://localhost:8080/ | jq .
# 登录
curl -X POST http://localhost:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"account":"demo","password":"demo123"}'
# 校验令牌(推荐第三方网关先调此接口)
curl -X POST http://localhost:8080/api/auth/verify \
-H 'Content-Type: application/json' \
-H 'X-Auth-Client: my-app' \
-d '{"token":"<jwt-token>"}'
# 同上,并携带应用展示名(累计应用接入记录)
curl -X POST http://localhost:8080/api/auth/verify \
-H 'Content-Type: application/json' \
-H 'X-Auth-Client: infogenie' \
-H 'X-Auth-Client-Name: 万象口袋' \
-d '{"token":"<jwt-token>"}'
# 使用令牌获取用户信息(会更新访问记录);可一并携带 X-Auth-Client 刷新「应用接入」最近时间
curl http://localhost:8080/api/auth/me \
-H 'Authorization: Bearer <jwt-token>' \
-H 'X-Auth-Client: infogenie' \
-H 'X-Auth-Client-Name: 万象口袋'