认证与会话
本章覆盖 HyperFRP Panel 后端的账号认证与会话管理接口,共 10 个:邮箱验证码发送、注册、登录(含两步验证)、密码重置、登出、当前用户信息查询以及会话列表 / 吊销 / 保活。
鉴权基调:除注册、登录等公开接口外,所有接口通过 Authorization: Bearer <token> 请求头携带 JWT;服务端在 Redis 中维护会话(Session),会话随验证与心跳滑动续期,修改密码会立即吊销全部会话。唯一例外是签到接口 POST /api/users/checkin:它额外接受访问密钥请求头 X-Access-Key 替代 JWT(见用户章节 · 签到),其余接口一律只认 JWT 会话。所有接口均返回统一包络 {"code": 0, "message": "...", "data": ...},错误码与 HTTP 状态码对齐(400 / 401 / 403 / 404 / 429 / 500);与具体接口无关的通用错误(参数缺失、未认证等)见 通用约定,Bearer 令牌的获取与使用方式详见 认证指南。本章全部路由经过 WAF 路由策略(RouteAuth)并显式禁止缓存(NoCache)。
注册与验证码
POST /api/auth/email-code
向指定邮箱发送 6 位数字验证码,验证码按用途隔离,用于注册或找回密码。
鉴权:无需认证(公开路由) · 限流:同一「用途 + 邮箱」60 秒内仅允许 1 次,超出返回 429 · 缓存:NoCache
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
email | string | 是 | 接收验证码的邮箱,须符合邮箱格式 |
captchaTicket | string | 否 | Cap.js 人机验证票据;binding 未强制必填,但缺失或校验不通过时会在人机验证环节被拒绝(400) |
purpose | string | 是 | 验证码用途,枚举:register(注册)、reset_password(找回密码) |
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/email-code \
-H "Content-Type: application/json" \
-d '{"email":"newuser@example.com","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9","purpose":"register"}'const res = await fetch('https://api.hyperfrp.com/api/auth/email-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"email":"newuser@example.com","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9","purpose":"register"})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/email-code',
json={
'email': 'newuser@example.com',
'captchaTicket': 'af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9',
'purpose': 'register'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"email": "newuser@example.com",
"captchaTicket": "af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9",
"purpose": "register",
})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/email-code", &payload)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
message | string | 固定为 验证码已发送 |
email | string | 脱敏后的邮箱:本地部分保留前 2 位、其余以 * 替代;本地部分不足 3 位时仅保留首 1 位 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "验证码已发送",
"email": "ne*******@example.com"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
参数绑定失败(邮箱格式错误、缺少 purpose 等) | 400 | 请输入有效的邮箱地址和验证码用途 |
purpose 不在 register / reset_password 枚举内 | 400 | 验证码用途无效 |
| Cap.js 人机验证未通过 | 400 | 人机验证未通过,请重试 |
用途为 register 但邮箱已被注册 | 400 | 该邮箱已被注册 |
用途为 reset_password 但邮箱未注册 | 400 | 该邮箱未注册 |
| 60 秒内对同一「用途 + 邮箱」重复请求 | 429 | 请求过于频繁,请稍后再试 |
| 验证码写入 Redis 失败或邮件发送失败 | 500 | 发送验证码失败,请稍后再试 |
通用错误见 通用约定。
注意事项
- 验证码为 6 位纯数字,写入后 5 分钟过期;注册与找回密码的验证码互不通用。
- 邮件服务未配置时接口仍返回成功,但验证码不会真正发出(仅记录一条脱敏告警日志);验证码本身不写入任何日志。
POST /api/auth/register
使用邮箱验证码注册新账号,注册成功后直接完成登录(返回令牌与用户信息)。
鉴权:无需认证(公开路由) · 缓存:NoCache
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 用户名,3~50 个字符,仅允许字母、数字、下划线 |
email | string | 是 | 邮箱,须为未注册邮箱,且与发送验证码时使用的邮箱一致 |
password | string | 是 | 密码,6~50 位,必须同时包含字母和数字 |
code | string | 是 | 6 位邮箱验证码(purpose=register 发送的那一枚) |
captchaTicket | string | 是 | Cap.js 人机验证票据 |
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"hyperzone","email":"newuser@example.com","password":"passw0rd","code":"482915","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"}'const res = await fetch('https://api.hyperfrp.com/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"username":"hyperzone","email":"newuser@example.com","password":"passw0rd","code":"482915","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/register',
json={
'username': 'hyperzone',
'email': 'newuser@example.com',
'password': 'passw0rd',
'code': '482915',
'captchaTicket': 'af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"username": "hyperzone",
"email": "newuser@example.com",
"password": "passw0rd",
"code": "482915",
"captchaTicket": "af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9",
})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/register", &payload)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
token | string | JWT 访问令牌 |
expiresIn | number | 令牌有效期(秒),默认 2592000(30 天),由服务端 JWT_EXPIRES_IN 配置决定 |
user | object | 新注册用户的完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMywicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.c9Xk1pRw7tQeBnM2aLdV0yHjG5sZfCoUxK8iTqNbE4g",
"expiresIn": 2592000,
"user": {
"id": 13,
"username": "hyperzone",
"email": "newuser@example.com",
"role": "user",
"status": "active",
"banReason": "",
"groupId": 1,
"group": {
"id": 1,
"name": "普通用户",
"description": "默认用户组",
"badgeColor": "#808080",
"isDefault": true,
"maxProxies": 5,
"bandwidthOutKbps": 512,
"bandwidthInKbps": 512,
"addedTrafficBytes": 0,
"addedBalance": 0,
"isVisible": false,
"isJoinable": false,
"linkedPackageId": null,
"createdAt": "2026-01-05T09:00:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"balance": 0,
"trafficUsedBytes": 0,
"remainingTrafficBytes": 0,
"isVerified": false,
"realNameVerificationStatus": "not_submitted",
"realNameVerificationTime": null,
"realNameVerificationMessage": "",
"realNameVerificationAttempts": 0,
"avatar": "https://placehold.co/150x150/aabbcc/ffffff?text=Avatar",
"lastLoginDate": "2026-09-19T10:35:00+08:00",
"lastLoginIp": "203.0.113.7",
"proxyCount": 0,
"qq": "",
"preferences": {},
"twoFactorEnabled": false,
"createdAt": "2026-09-19T10:35:00+08:00",
"updatedAt": "2026-09-19T10:35:00+08:00"
}
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 参数绑定失败(缺字段、长度不符、验证码不是 6 位等) | 400 | 请填写完整的注册信息 |
| Cap.js 人机验证未通过 | 400 | 人机验证未通过,请重试 |
| 用户名含字母、数字、下划线之外的字符 | 400 | 用户名只能包含字母、数字和下划线 |
| 密码少于 6 位或未同时包含字母和数字 | 400 | 密码必须包含字母和数字,长度至少6位 |
| 验证码错误或已过期(超过 5 分钟) | 400 | 验证码无效或已过期 |
| 用户名已被使用 | 400 | 用户名已被使用 |
| 邮箱已被注册 | 400 | 邮箱已被注册 |
| 默认用户组缺失且自动创建失败 / 创建用户失败 | 500 | 注册失败,请稍后再试 |
| 生成 JWT 令牌失败 | 500 | 生成令牌失败 |
| 创建登录会话失败 | 500 | 创建登录会话失败,请稍后重试 |
通用错误见 通用约定。
注意事项
- 新用户角色固定为
user、状态为active,并加入默认用户组(isDefault=true);若默认用户组不存在会自动创建「普通用户」(maxProxies=5)。 - 注册成功即建立服务端会话,同时记录
last_login_at/last_login_ip;邮箱验证码校验通过后立即从 Redis 删除,一次性使用。 - 密码以 bcrypt 哈希存储,接口层不回传任何密码信息。
登录
POST /api/auth/login
账号密码登录。普通用户直接返回令牌;已开启两步验证的用户返回临时令牌,需继续调用 POST /api/auth/2fa/verify 完成登录。
鉴权:无需认证(公开路由) · 限流:同一登录标识有频率限制(成功登录后计数清零,超出返回 429) · 缓存:NoCache
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 三者至少其一 | 登录标识:用户名,非空时优先级最高 |
emailOrUsername | string | 三者至少其一 | 登录标识:邮箱或用户名 |
email | string | 三者至少其一 | 登录标识:邮箱,优先级最低 |
password | string | 是 | 密码 |
captcha | string | 否 | 兼容字段,当前版本登录校验不读取该字段 |
captchaTicket | string | 是 | Cap.js 人机验证票据 |
三个标识字段均为可选键,但至少提供一个:服务端按 username → emailOrUsername → email 的顺序取第一个非空值作为登录标识,三者都缺失或都为空时返回 400(请输入用户名和密码)。
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"emailOrUsername":"hyperzone","password":"passw0rd","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"}'const res = await fetch('https://api.hyperfrp.com/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"emailOrUsername":"hyperzone","password":"passw0rd","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/login',
json={
'emailOrUsername': 'hyperzone',
'password': 'passw0rd',
'captchaTicket': 'af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"emailOrUsername": "hyperzone",
"password": "passw0rd",
"captchaTicket": "af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9",
})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/login", &payload)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
除 requires2FA / tempToken 分支外,各字段均带 omitempty 语义,未出现的键不会出现在 JSON 中。
| 字段 | 类型 | 说明 |
|---|---|---|
token | string | JWT 访问令牌(登录成功时返回) |
expiresIn | number | 令牌有效期(秒),默认 2592000(30 天) |
user | object | 用户完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表 |
requires2FA | boolean | 为 true 表示该用户已开启两步验证,须携带 tempToken 调用 POST /api/auth/2fa/verify |
tempToken | string | 两步验证临时令牌(Base64URL 编码的 32 字节随机值),有效期 5 分钟 |
响应示例
登录成功(未开启两步验证):
{
"code": 0,
"message": "成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o",
"expiresIn": 2592000,
"user": {
"id": 12,
"username": "hyperzone",
"email": "user@example.com",
"role": "user",
"status": "active",
"banReason": "",
"groupId": 1,
"group": {
"id": 1,
"name": "普通用户",
"description": "默认用户组",
"badgeColor": "#808080",
"isDefault": true,
"maxProxies": 5,
"bandwidthOutKbps": 512,
"bandwidthInKbps": 512,
"addedTrafficBytes": 0,
"addedBalance": 0,
"isVisible": false,
"isJoinable": false,
"linkedPackageId": null,
"createdAt": "2026-01-05T09:00:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"balance": 12.5,
"trafficUsedBytes": 4294967296,
"remainingTrafficBytes": 6442450944,
"isVerified": true,
"realNameVerificationStatus": "approved",
"avatar": "https://placehold.co/150x150/aabbcc/ffffff?text=Avatar",
"lastLoginDate": "2026-09-19T10:30:00+08:00",
"lastLoginIp": "203.0.113.7",
"proxyCount": 2,
"qq": "",
"preferences": { "theme": "dark" },
"twoFactorEnabled": false,
"createdAt": "2026-01-05T09:10:00+08:00",
"updatedAt": "2026-09-19T10:30:00+08:00"
}
}
}已开启两步验证的用户返回:
{
"code": 0,
"message": "成功",
"data": {
"requires2FA": true,
"tempToken": "Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
参数绑定失败(缺 password / captchaTicket),或三个标识字段全为空 | 400 | 请输入用户名和密码 |
| Cap.js 人机验证未通过 | 400 | 人机验证未通过,请重试 |
| 同一登录标识 1 小时内尝试超过 5 次 | 429 | 登录尝试次数过多,请稍后再试 |
| 登录标识对应的用户不存在 | 401 | 用户名或密码错误 |
| 密码错误 | 401 | 用户名或密码错误 |
| 账户已被封禁 | 403 | 您的账户已被封禁,data 中含 isBanned 与 ban_reason,见下表 |
| 生成 JWT 令牌失败 | 500 | 生成令牌失败 |
| 创建登录会话失败 | 500 | 创建登录会话失败,请稍后重试 |
封禁用户响应的 data 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
isBanned | boolean | 固定为 true |
ban_reason | string | 封禁原因,可能是空串 |
通用错误见 通用约定。
注意事项
- 登录成功后的副作用:创建服务端会话、更新
last_login_at/last_login_ip、写入审计与用户日志;邮件服务已配置时还会向用户邮箱发送登录提醒(含 IP、归属地、设备与时间)。 - 已开启两步验证的分支不创建会话、不下发正式令牌;
tempToken存储在 Redis(键为令牌 SHA-256 摘要),5 分钟过期,且不计入登录限流清零。 - 登录失败(用户不存在、密码错误)与登录成功对外消息一致(401 均为
用户名或密码错误),不暴露账号是否存在。 - 限流计数按「登录标识」维度独立统计,成功登录后该标识的计数清零。
POST /api/auth/2fa/verify
提交两步验证验证码完成登录,成功后返回与登录接口一致的令牌和用户信息。
鉴权:无需认证(公开路由,凭 tempToken 换取正式令牌) · 限流:单个临时令牌有验证次数上限,超限后挑战作废并返回 429 · 缓存:NoCache
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tempToken | string | 是 | 登录接口返回的两步验证临时令牌(Base64URL 编码的 32 字节值,长度 43 字符起) |
code | string | 是 | 两步验证验证码:TOTP 动态码,或一枚未使用的恢复码(恢复码使用后即作废) |
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/2fa/verify \
-H "Content-Type: application/json" \
-d '{"tempToken":"Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6","code":"184930"}'const res = await fetch('https://api.hyperfrp.com/api/auth/2fa/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"tempToken":"Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6","code":"184930"})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/2fa/verify',
json={
'tempToken': 'Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6',
'code': '184930'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"tempToken": "Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6",
"code": "184930",
})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/2fa/verify", &payload)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
与登录成功响应完全一致(requires2FA / tempToken 不再出现):
| 字段 | 类型 | 说明 |
|---|---|---|
token | string | JWT 访问令牌 |
expiresIn | number | 令牌有效期(秒),默认 2592000(30 天) |
user | object | 用户完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o",
"expiresIn": 2592000,
"user": {
"id": 12,
"username": "hyperzone",
"email": "user@example.com",
"role": "user",
"status": "active",
"groupId": 1,
"group": {
"id": 1,
"name": "普通用户",
"description": "默认用户组",
"badgeColor": "#808080",
"isDefault": true,
"maxProxies": 5,
"bandwidthOutKbps": 512,
"bandwidthInKbps": 512,
"addedTrafficBytes": 0,
"addedBalance": 0,
"isVisible": false,
"isJoinable": false,
"linkedPackageId": null,
"createdAt": "2026-01-05T09:00:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"twoFactorEnabled": true,
"lastLoginDate": "2026-09-19T10:32:00+08:00",
"lastLoginIp": "203.0.113.7",
"preferences": { "theme": "dark" },
"createdAt": "2026-01-05T09:10:00+08:00",
"updatedAt": "2026-09-19T10:32:00+08:00"
}
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
参数绑定失败(缺 tempToken 或 code) | 400 | 请输入临时令牌和验证码 |
| 临时令牌长度不足 43,或对应的挑战不存在 / 已过期 | 401 | 临时令牌无效或已过期 |
| 同一临时令牌验证尝试超过 5 次 | 429 | 验证尝试次数过多,请稍后再试 |
| 验证码错误(TOTP 不匹配且无可用恢复码) | 400 | 验证码错误 |
| 临时令牌已被使用(重复提交) | 401 | 临时令牌已被使用或已过期 |
账户状态非 active,或两步验证已被关闭 | 401 | 账户状态或两步验证设置已变化,请重新登录 |
| Redis 挑战更新失败 / 验证码校验异常 | 500 | 更新验证挑战失败 / 验证码校验失败 |
| 生成 JWT 令牌失败 | 500 | 生成令牌失败 |
| 创建登录会话失败 | 500 | 创建登录会话失败,请重新登录 |
通用错误见 通用约定。
注意事项
tempToken一次性使用:验证成功后立即从 Redis 删除,重复提交返回 401;尝试次数超限时挑战被直接删除,令牌随之作废。- 验证成功后会对该登录标识的限流计数清零,避免已通过验证的用户仍被登录限流锁住。
- 与密码登录一样:成功即创建会话、更新最近登录信息,并写
login_2fa用户日志。
密码重置
PUT /api/auth/reset-password
通过邮箱验证码重置密码。成功后该用户全部会话被吊销、旧 JWT 全部失效,需重新登录。
鉴权:无需认证(公开路由) · 缓存:NoCache
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
email | string | 是 | 已注册邮箱,须与发送验证码时使用的邮箱一致 |
code | string | 是 | 6 位邮箱验证码(purpose=reset_password 发送的那一枚) |
password | string | 否 | 新密码;与 newPassword 二选一,两者同时提供时优先使用本字段 |
newPassword | string | 否 | 新密码;password 为空时的替代字段 |
captchaTicket | string | 是 | Cap.js 人机验证票据 |
两个密码字段在服务端取值后统一校验:至少 6 位且同时包含字母和数字,不满足返回 400。
请求示例
curl -X PUT https://api.hyperfrp.com/api/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","code":"573812","newPassword":"n3wpassw0rd","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"}'const res = await fetch('https://api.hyperfrp.com/api/auth/reset-password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"email":"user@example.com","code":"573812","newPassword":"n3wpassw0rd","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"})
});
const data = await res.json();
console.log(data);import requests
res = requests.put(
'https://api.hyperfrp.com/api/auth/reset-password',
json={
'email': 'user@example.com',
'code': '573812',
'newPassword': 'n3wpassw0rd',
'captchaTicket': 'af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"email": "user@example.com",
"code": "573812",
"newPassword": "n3wpassw0rd",
"captchaTicket": "af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9",
})
req, _ := http.NewRequest("PUT", "https://api.hyperfrp.com/api/auth/reset-password", &payload)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
message | string | 固定为 密码重置成功,请重新登录 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "密码重置成功,请重新登录"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
参数绑定失败(缺 email / code / captchaTicket 等) | 400 | 请填写完整的信息 |
| 新密码少于 6 位或未同时包含字母和数字 | 400 | 密码必须包含字母和数字,长度至少6位 |
| Cap.js 人机验证未通过 | 400 | 人机验证未通过,请重试 |
| 验证码错误或已过期 | 400 | 验证码无效或已过期 |
| 邮箱未注册 | 400 | 该邮箱未注册 |
| 密码哈希计算失败 | 500 | 密码设置失败 |
| 用户信息保存失败 | 500 | 密码重置失败,请稍后再试 |
通用错误见 通用约定。
注意事项
- 重置成功后用户的
authVersion自增(此前签发的全部 JWT 失效),并吊销该用户在所有设备上的会话。 - 验证码校验通过后立即从 Redis 删除,一次性使用;
password与newPassword两个字段名并存是为兼容不同时期的客户端。
登出
POST /api/auth/logout
登出并删除当前令牌对应的服务端会话。该接口被有意放在公开路由:即使 JWT 已过期也能删除会话,避免会话残留。
鉴权:Bearer 可选(Authorization 头缺省时接口仍返回成功) · 缓存:NoCache
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"const res = await fetch('https://api.hyperfrp.com/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/logout',
headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/logout", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
message | string | 固定为 已成功登出 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "已成功登出"
}
}错误场景
无业务错误分支:无论请求头中是否携带令牌、令牌是否有效,均返回成功。通用错误见 通用约定。
注意事项
- 令牌仅从
Authorization: Bearer请求头读取,不接受 URL Query 传参。 - 只要头中携带了令牌就会尝试删除对应会话(即使 JWT 已过期或校验失败),删除后同时清理用户会话列表索引。
- 能读到会话归属信息时,会尽力写入审计与用户登出日志;本接口幂等,重复调用结果一致。
当前用户与会话管理
GET /api/auth/me
查询当前登录用户的完整信息(含预载的用户组)。
鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache
请求示例
curl https://api.hyperfrp.com/api/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"const res = await fetch('https://api.hyperfrp.com/api/auth/me', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/auth/me',
headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/auth/me", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
data 为完整用户对象(models.User 序列化):
| 字段 | 类型 | 说明 |
|---|---|---|
id | number | 用户 ID |
username | string | 用户名 |
email | string | 邮箱 |
role | string | 角色:user / admin / superadmin |
status | string | 状态:active / banned / inactive |
banReason | string | 封禁原因,未封禁时为空串 |
groupId | number | 所属用户组 ID |
group | object | 用户组完整信息,字段见下表 |
groupExpiresAt | string | 用户组(套餐)到期时间;为空时该键不出现 |
accessKey | string | 注册时生成的 32 位随机接入密钥 |
balance | number | 账户余额 |
traffic | number | 非持久化字段,本接口不填充(恒为 0) |
trafficUsedBytes | number | 已用流量(字节) |
remainingTrafficBytes | number | 剩余流量(字节) |
usedTraffic | number | 非持久化字段,本接口不填充(恒为 0) |
realName | string | 实名姓名;为空时该键不出现 |
idCardNumber | string | 明文身份证号;仅服务端配置 RSA 私钥且解密成功时出现 |
isVerified | boolean | 是否已通过实名认证 |
realNameVerificationStatus | string | 实名审核状态:not_submitted / pending / approved / rejected / revoked(未提交过时为空串) |
realNameVerificationTime | string/null | 实名审核时间 |
realNameVerificationMessage | string | 实名审核备注 |
realNameVerificationAttempts | number | 实名申请次数 |
avatar | string | 头像 URL,默认指向占位图 |
lastSignInDate | string | 最近签到日期;为空时该键不出现 |
lastLoginDate | string | 最近登录时间;为空时该键不出现 |
lastLoginIp | string | 最近登录 IP |
maxProxies | number | 非持久化字段,本接口不填充(恒为 0),隧道配额以 group.maxProxies 为准 |
proxyCount | number | 当前隧道数量 |
qq | string | QQ 号 |
preferences | object | 用户偏好设置(JSON 对象,无内容时为 {}) |
twoFactorEnabled | boolean | 是否已开启两步验证 |
remark | string | 非持久化字段,本接口不填充(恒为空串) |
createdAt | string | 注册时间 |
updatedAt | string | 信息更新时间 |
group 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | number | 用户组 ID |
name | string | 用户组名称 |
description | string | 用户组描述 |
badgeColor | string | 徽章颜色,默认 #808080 |
isDefault | boolean | 是否默认用户组 |
maxProxies | number | 最大隧道数,-1 表示不限制 |
bandwidthOutKbps | number | 出站带宽上限(Kbps) |
bandwidthInKbps | number | 入站带宽上限(Kbps) |
addedTrafficBytes | number | 用户组附加流量(字节) |
addedBalance | number | 用户组附加余额 |
isVisible | boolean | 是否在套餐商店可见 |
isJoinable | boolean | 是否允许用户主动加入 |
linkedPackageId | number/null | 关联套餐 ID |
createdAt | string | 创建时间 |
updatedAt | string | 更新时间 |
以下字段被标记 json:"-",任何情况下都不会出现在响应中:password(bcrypt 哈希)、idCardNumber(数据库中的密文身份证号)、verificationProofUrl(实名凭证地址)、authVersion(凭证版本)。
响应示例
{
"code": 0,
"message": "成功",
"data": {
"id": 12,
"username": "hyperzone",
"email": "user@example.com",
"role": "user",
"status": "active",
"banReason": "",
"groupId": 1,
"group": {
"id": 1,
"name": "普通用户",
"description": "默认用户组",
"badgeColor": "#808080",
"isDefault": true,
"maxProxies": 5,
"bandwidthOutKbps": 512,
"bandwidthInKbps": 512,
"addedTrafficBytes": 0,
"addedBalance": 0,
"isVisible": false,
"isJoinable": false,
"linkedPackageId": null,
"createdAt": "2026-01-05T09:00:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"groupExpiresAt": "2027-01-05T09:00:00+08:00",
"accessKey": "kX9mQ2vLtR7wZpA4sYb1cJd6fHg3nEu0",
"balance": 12.5,
"traffic": 0,
"trafficUsedBytes": 4294967296,
"remainingTrafficBytes": 6442450944,
"usedTraffic": 0,
"realName": "张三",
"idCardNumber": "110101199001011234",
"isVerified": true,
"realNameVerificationStatus": "approved",
"realNameVerificationTime": "2026-03-10T14:20:00+08:00",
"realNameVerificationMessage": "",
"realNameVerificationAttempts": 1,
"avatar": "https://placehold.co/150x150/aabbcc/ffffff?text=Avatar",
"lastSignInDate": "2026-09-18T00:00:00+08:00",
"lastLoginDate": "2026-09-19T10:30:00+08:00",
"lastLoginIp": "203.0.113.7",
"maxProxies": 0,
"proxyCount": 2,
"qq": "12345678",
"preferences": { "theme": "dark", "notifyLogin": true },
"twoFactorEnabled": true,
"remark": "",
"createdAt": "2026-01-05T09:10:00+08:00",
"updatedAt": "2026-09-19T10:30:00+08:00"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 令牌无效、过期或未携带 | 401 | 未认证(由认证中间件或处理器返回) |
通用错误见 通用约定。
注意事项
- 响应用当前令牌对应的用户 ID 重新查询数据库并预载
group,返回的是最新数据而非令牌签发时的快照。 - 示例中的
idCardNumber仅在服务端配置了 RSA 私钥时可能出现;生产环境按设计只部署公钥,该字段通常不出现。
GET /api/auth/sessions
查询当前用户在所有设备上的登录会话列表。
鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache
请求示例
curl https://api.hyperfrp.com/api/auth/sessions \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"const res = await fetch('https://api.hyperfrp.com/api/auth/sessions', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/auth/sessions',
headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/auth/sessions", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
data 为会话对象数组:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 会话标识:令牌 SHA-256 摘要前 8 字节的十六进制(16 字符),吊销会话时用作路径参数 |
ip | string | 登录 IP |
ipLocation | string | IP 归属地(国家 · 省份 · 城市,无法解析时为 未知位置) |
browserName | string | 浏览器名称(由 User-Agent 解析) |
browserVersion | string | 浏览器版本 |
osName | string | 操作系统 |
osVersion | string | 操作系统版本 |
deviceType | string | 设备类型(如 desktop / mobile) |
clientType | string | 客户端类型,当前固定为 WEB |
loginTime | string | 登录时间(RFC3339) |
lastActiveTime | string | 最近活跃时间(RFC3339) |
shortToken | string | 脱敏令牌:完整 JWT 超过 15 字符时取前 10 位 + ... + 后 5 位 |
isCurrent | boolean | 是否为当前请求所使用的会话 |
isOnline | boolean | 最近 10 分钟内是否有活动 |
响应示例
{
"code": 0,
"message": "成功",
"data": [
{
"id": "9f86d081884c7d65",
"ip": "203.0.113.7",
"ipLocation": "中国 · 上海市 · 上海",
"browserName": "Chrome",
"browserVersion": "131.0.0.0",
"osName": "Windows",
"osVersion": "10",
"deviceType": "desktop",
"clientType": "WEB",
"loginTime": "2026-09-19T08:12:00+08:00",
"lastActiveTime": "2026-09-19T10:30:00+08:00",
"shortToken": "eyJhbGciOi...kNqM1o",
"isCurrent": true,
"isOnline": true
},
{
"id": "2c26b46b68ffc68f",
"ip": "198.51.100.23",
"ipLocation": "中国 · 广东省 · 深圳",
"browserName": "Firefox",
"browserVersion": "133.0",
"osName": "macOS",
"osVersion": "14.6",
"deviceType": "desktop",
"clientType": "WEB",
"loginTime": "2026-09-15T21:40:00+08:00",
"lastActiveTime": "2026-09-17T19:05:00+08:00",
"shortToken": "eyJhbGciOi...bE4g2a",
"isCurrent": false,
"isOnline": false
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 令牌无效、过期或未携带 | 401 | 未认证 |
| 会话列表读取失败 | 500 | 获取会话列表失败 |
通用错误见 通用约定。
注意事项
- 列表只包含当前用户自己的会话,按服务端会话存储顺序返回,不保证固定排序。
isOnline是基于「最近活跃时间在 10 分钟内」的推断值,并非实时连接状态。
DELETE /api/auth/sessions/:sessionId
吊销(下线)当前用户的指定会话。
鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
sessionId | string | 会话标识,取自会话列表接口返回的 id(16 位十六进制哈希) |
请求示例
curl -X DELETE https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"const res = await fetch('https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.delete(
'https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f',
headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("DELETE", "https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
message | string | 固定为 会话已撤销 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "会话已撤销"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 令牌无效、过期或未携带 | 401 | 未认证 |
| 会话不存在、已失效或不属于当前用户 | 404 | 会话不存在或已失效 |
| 会话撤销执行失败(服务端存储异常等) | 400 | 会话撤销失败,请稍后重试 |
通用错误见 通用约定。
注意事项
- 仅能吊销属于当前用户自己的会话,跨用户吊销一律按
404 会话不存在或已失效处理,不泄露会话存在性。 - 吊销当前请求正在使用的会话等效于登出,该令牌随后的请求都会因会话失效而 401。
- 吊销成功会写入
revoke_session用户日志(含被吊销的会话标识)。
POST /api/auth/sessions/heartbeat
当前会话保活:刷新最近活跃时间并滑动会话有效期。
鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache
请求示例
curl -X POST https://api.hyperfrp.com/api/auth/sessions/heartbeat \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"const res = await fetch('https://api.hyperfrp.com/api/auth/sessions/heartbeat', {
method: 'POST',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/auth/sessions/heartbeat',
headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/auth/sessions/heartbeat", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
message | string | 固定为 心跳成功 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "心跳成功"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
未携带 Authorization 头 | 401 | 未提供认证令牌 |
| 会话不存在或已过期 | 401 | 会话无效或已过期 |
通用错误见 通用约定。
注意事项
- 心跳会更新会话的最近活跃时间,并将会话键与用户会话列表的 TTL 同步滑动重置——持续心跳的会话不会因过期被清理。
- 客户端定时调用心跳可同时维持会话列表中的
isOnline判定(10 分钟内活跃即视为在线)。 - 令牌仅从
Authorization: Bearer请求头读取,不接受 URL Query 传参。