Skip to content

认证与会话

本章覆盖 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

请求体

字段类型必填说明
emailstring接收验证码的邮箱,须符合邮箱格式
captchaTicketstringCap.js 人机验证票据;binding 未强制必填,但缺失或校验不通过时会在人机验证环节被拒绝(400)
purposestring验证码用途,枚举:register(注册)、reset_password(找回密码)

请求示例

bash
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"}'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为 验证码已发送
emailstring脱敏后的邮箱:本地部分保留前 2 位、其余以 * 替代;本地部分不足 3 位时仅保留首 1 位

响应示例

json
{
  "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

请求体

字段类型必填说明
usernamestring用户名,3~50 个字符,仅允许字母、数字、下划线
emailstring邮箱,须为未注册邮箱,且与发送验证码时使用的邮箱一致
passwordstring密码,6~50 位,必须同时包含字母和数字
codestring6 位邮箱验证码(purpose=register 发送的那一枚)
captchaTicketstringCap.js 人机验证票据

请求示例

bash
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"}'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
tokenstringJWT 访问令牌
expiresInnumber令牌有效期(秒),默认 2592000(30 天),由服务端 JWT_EXPIRES_IN 配置决定
userobject新注册用户的完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表

响应示例

json
{
  "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

请求体

字段类型必填说明
usernamestring三者至少其一登录标识:用户名,非空时优先级最高
emailOrUsernamestring三者至少其一登录标识:邮箱或用户名
emailstring三者至少其一登录标识:邮箱,优先级最低
passwordstring密码
captchastring兼容字段,当前版本登录校验不读取该字段
captchaTicketstringCap.js 人机验证票据

三个标识字段均为可选键,但至少提供一个:服务端按 usernameemailOrUsernameemail 的顺序取第一个非空值作为登录标识,三者都缺失或都为空时返回 400(请输入用户名和密码)。

请求示例

bash
curl -X POST https://api.hyperfrp.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"emailOrUsername":"hyperzone","password":"passw0rd","captchaTicket":"af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9"}'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/auth/login',
    json={
        'emailOrUsername': 'hyperzone',
        'password': 'passw0rd',
        'captchaTicket': 'af7c2f5b9d3e48c1a0b6d8e2f4a1c7d9'
    },
)
data = res.json()
print(data)
go
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 中。

字段类型说明
tokenstringJWT 访问令牌(登录成功时返回)
expiresInnumber令牌有效期(秒),默认 2592000(30 天)
userobject用户完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表
requires2FAbooleantrue 表示该用户已开启两步验证,须携带 tempToken 调用 POST /api/auth/2fa/verify
tempTokenstring两步验证临时令牌(Base64URL 编码的 32 字节随机值),有效期 5 分钟

响应示例

登录成功(未开启两步验证):

json
{
  "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"
    }
  }
}

已开启两步验证的用户返回:

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "requires2FA": true,
    "tempToken": "Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6"
  }
}

错误场景

场景HTTP说明
参数绑定失败(缺 password / captchaTicket),或三个标识字段全为空400请输入用户名和密码
Cap.js 人机验证未通过400人机验证未通过,请重试
同一登录标识 1 小时内尝试超过 5 次429登录尝试次数过多,请稍后再试
登录标识对应的用户不存在401用户名或密码错误
密码错误401用户名或密码错误
账户已被封禁403您的账户已被封禁data 中含 isBannedban_reason,见下表
生成 JWT 令牌失败500生成令牌失败
创建登录会话失败500创建登录会话失败,请稍后重试

封禁用户响应的 data 结构:

字段类型说明
isBannedboolean固定为 true
ban_reasonstring封禁原因,可能是空串

通用错误见 通用约定

注意事项

  • 登录成功后的副作用:创建服务端会话、更新 last_login_at / last_login_ip、写入审计与用户日志;邮件服务已配置时还会向用户邮箱发送登录提醒(含 IP、归属地、设备与时间)。
  • 已开启两步验证的分支不创建会话、不下发正式令牌;tempToken 存储在 Redis(键为令牌 SHA-256 摘要),5 分钟过期,且不计入登录限流清零。
  • 登录失败(用户不存在、密码错误)与登录成功对外消息一致(401 均为 用户名或密码错误),不暴露账号是否存在。
  • 限流计数按「登录标识」维度独立统计,成功登录后该标识的计数清零。

POST /api/auth/2fa/verify

提交两步验证验证码完成登录,成功后返回与登录接口一致的令牌和用户信息。

鉴权:无需认证(公开路由,凭 tempToken 换取正式令牌) · 限流:单个临时令牌有验证次数上限,超限后挑战作废并返回 429 · 缓存:NoCache

请求体

字段类型必填说明
tempTokenstring登录接口返回的两步验证临时令牌(Base64URL 编码的 32 字节值,长度 43 字符起)
codestring两步验证验证码:TOTP 动态码,或一枚未使用的恢复码(恢复码使用后即作废)

请求示例

bash
curl -X POST https://api.hyperfrp.com/api/auth/2fa/verify \
  -H "Content-Type: application/json" \
  -d '{"tempToken":"Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6","code":"184930"}'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/auth/2fa/verify',
    json={
        'tempToken': 'Hx9Jk2LmNp4Qr6StUv8Wy0zA1bC3dE5fG7hI9jK0lM2nO4pQ6rS8tU0vW2xY4z6',
        'code': '184930'
    },
)
data = res.json()
print(data)
go
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 不再出现):

字段类型说明
tokenstringJWT 访问令牌
expiresInnumber令牌有效期(秒),默认 2592000(30 天)
userobject用户完整信息(含预载的用户组 group),字段结构见「GET /api/auth/me」的响应字段表

响应示例

json
{
  "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说明
参数绑定失败(缺 tempTokencode400请输入临时令牌和验证码
临时令牌长度不足 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

请求体

字段类型必填说明
emailstring已注册邮箱,须与发送验证码时使用的邮箱一致
codestring6 位邮箱验证码(purpose=reset_password 发送的那一枚)
passwordstring新密码;与 newPassword 二选一,两者同时提供时优先使用本字段
newPasswordstring新密码;password 为空时的替代字段
captchaTicketstringCap.js 人机验证票据

两个密码字段在服务端取值后统一校验:至少 6 位且同时包含字母和数字,不满足返回 400。

请求示例

bash
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"}'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为 密码重置成功,请重新登录

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "密码重置成功,请重新登录"
  }
}

错误场景

场景HTTP说明
参数绑定失败(缺 email / code / captchaTicket 等)400请填写完整的信息
新密码少于 6 位或未同时包含字母和数字400密码必须包含字母和数字,长度至少6位
Cap.js 人机验证未通过400人机验证未通过,请重试
验证码错误或已过期400验证码无效或已过期
邮箱未注册400该邮箱未注册
密码哈希计算失败500密码设置失败
用户信息保存失败500密码重置失败,请稍后再试

通用错误见 通用约定

注意事项

  • 重置成功后用户的 authVersion 自增(此前签发的全部 JWT 失效),并吊销该用户在所有设备上的会话。
  • 验证码校验通过后立即从 Redis 删除,一次性使用;passwordnewPassword 两个字段名并存是为兼容不同时期的客户端。

登出

POST /api/auth/logout

登出并删除当前令牌对应的服务端会话。该接口被有意放在公开路由:即使 JWT 已过期也能删除会话,避免会话残留。

鉴权:Bearer 可选(Authorization 头缺省时接口仍返回成功) · 缓存:NoCache

请求示例

bash
curl -X POST https://api.hyperfrp.com/api/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/auth/logout',
    headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)
go
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)
}

响应字段

字段类型说明
messagestring固定为 已成功登出

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "已成功登出"
  }
}

错误场景

无业务错误分支:无论请求头中是否携带令牌、令牌是否有效,均返回成功。通用错误见 通用约定

注意事项

  • 令牌仅从 Authorization: Bearer 请求头读取,不接受 URL Query 传参。
  • 只要头中携带了令牌就会尝试删除对应会话(即使 JWT 已过期或校验失败),删除后同时清理用户会话列表索引。
  • 能读到会话归属信息时,会尽力写入审计与用户登出日志;本接口幂等,重复调用结果一致。

当前用户与会话管理

GET /api/auth/me

查询当前登录用户的完整信息(含预载的用户组)。

鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache

请求示例

bash
curl https://api.hyperfrp.com/api/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"
javascript
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);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/auth/me',
    headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)
go
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 序列化):

字段类型说明
idnumber用户 ID
usernamestring用户名
emailstring邮箱
rolestring角色:user / admin / superadmin
statusstring状态:active / banned / inactive
banReasonstring封禁原因,未封禁时为空串
groupIdnumber所属用户组 ID
groupobject用户组完整信息,字段见下表
groupExpiresAtstring用户组(套餐)到期时间;为空时该键不出现
accessKeystring注册时生成的 32 位随机接入密钥
balancenumber账户余额
trafficnumber非持久化字段,本接口不填充(恒为 0)
trafficUsedBytesnumber已用流量(字节)
remainingTrafficBytesnumber剩余流量(字节)
usedTrafficnumber非持久化字段,本接口不填充(恒为 0)
realNamestring实名姓名;为空时该键不出现
idCardNumberstring明文身份证号;仅服务端配置 RSA 私钥且解密成功时出现
isVerifiedboolean是否已通过实名认证
realNameVerificationStatusstring实名审核状态:not_submitted / pending / approved / rejected / revoked(未提交过时为空串)
realNameVerificationTimestring/null实名审核时间
realNameVerificationMessagestring实名审核备注
realNameVerificationAttemptsnumber实名申请次数
avatarstring头像 URL,默认指向占位图
lastSignInDatestring最近签到日期;为空时该键不出现
lastLoginDatestring最近登录时间;为空时该键不出现
lastLoginIpstring最近登录 IP
maxProxiesnumber非持久化字段,本接口不填充(恒为 0),隧道配额以 group.maxProxies 为准
proxyCountnumber当前隧道数量
qqstringQQ 号
preferencesobject用户偏好设置(JSON 对象,无内容时为 {}
twoFactorEnabledboolean是否已开启两步验证
remarkstring非持久化字段,本接口不填充(恒为空串)
createdAtstring注册时间
updatedAtstring信息更新时间

group 对象字段:

字段类型说明
idnumber用户组 ID
namestring用户组名称
descriptionstring用户组描述
badgeColorstring徽章颜色,默认 #808080
isDefaultboolean是否默认用户组
maxProxiesnumber最大隧道数,-1 表示不限制
bandwidthOutKbpsnumber出站带宽上限(Kbps)
bandwidthInKbpsnumber入站带宽上限(Kbps)
addedTrafficBytesnumber用户组附加流量(字节)
addedBalancenumber用户组附加余额
isVisibleboolean是否在套餐商店可见
isJoinableboolean是否允许用户主动加入
linkedPackageIdnumber/null关联套餐 ID
createdAtstring创建时间
updatedAtstring更新时间

以下字段被标记 json:"-",任何情况下都不会出现在响应中:password(bcrypt 哈希)、idCardNumber(数据库中的密文身份证号)、verificationProofUrl(实名凭证地址)、authVersion(凭证版本)。

响应示例

json
{
  "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

请求示例

bash
curl https://api.hyperfrp.com/api/auth/sessions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"
javascript
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);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/auth/sessions',
    headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)
go
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 为会话对象数组:

字段类型说明
idstring会话标识:令牌 SHA-256 摘要前 8 字节的十六进制(16 字符),吊销会话时用作路径参数
ipstring登录 IP
ipLocationstringIP 归属地(国家 · 省份 · 城市,无法解析时为 未知位置
browserNamestring浏览器名称(由 User-Agent 解析)
browserVersionstring浏览器版本
osNamestring操作系统
osVersionstring操作系统版本
deviceTypestring设备类型(如 desktop / mobile
clientTypestring客户端类型,当前固定为 WEB
loginTimestring登录时间(RFC3339)
lastActiveTimestring最近活跃时间(RFC3339)
shortTokenstring脱敏令牌:完整 JWT 超过 15 字符时取前 10 位 + ... + 后 5 位
isCurrentboolean是否为当前请求所使用的会话
isOnlineboolean最近 10 分钟内是否有活动

响应示例

json
{
  "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

路径参数

参数类型说明
sessionIdstring会话标识,取自会话列表接口返回的 id(16 位十六进制哈希)

请求示例

bash
curl -X DELETE https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"
javascript
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);
python
import requests

res = requests.delete(
    'https://api.hyperfrp.com/api/auth/sessions/2c26b46b68ffc68f',
    headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)
go
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)
}

响应字段

字段类型说明
messagestring固定为 会话已撤销

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "会话已撤销"
  }
}

错误场景

场景HTTP说明
令牌无效、过期或未携带401未认证
会话不存在、已失效或不属于当前用户404会话不存在或已失效
会话撤销执行失败(服务端存储异常等)400会话撤销失败,请稍后重试

通用错误见 通用约定

注意事项

  • 仅能吊销属于当前用户自己的会话,跨用户吊销一律按 404 会话不存在或已失效 处理,不泄露会话存在性。
  • 吊销当前请求正在使用的会话等效于登出,该令牌随后的请求都会因会话失效而 401。
  • 吊销成功会写入 revoke_session 用户日志(含被吊销的会话标识)。

POST /api/auth/sessions/heartbeat

当前会话保活:刷新最近活跃时间并滑动会话有效期。

鉴权:Bearer JWT(authMiddleware.Protect) · 缓存:NoCache

请求示例

bash
curl -X POST https://api.hyperfrp.com/api/auth/sessions/heartbeat \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o"
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/auth/sessions/heartbeat',
    headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMiwicm9sZSI6InVzZXIiLCJhdXRoX3ZlcnNpb24iOjF9.q3Vw8xZnTeKb2mLfGd9sYcA0pHrUuJo5iW7eXkNqM1o'},
)
data = res.json()
print(data)
go
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)
}

响应字段

字段类型说明
messagestring固定为 心跳成功

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "心跳成功"
  }
}

错误场景

场景HTTP说明
未携带 Authorization401未提供认证令牌
会话不存在或已过期401会话无效或已过期

通用错误见 通用约定

注意事项

  • 心跳会更新会话的最近活跃时间,并将会话键与用户会话列表的 TTL 同步滑动重置——持续心跳的会话不会因过期被清理。
  • 客户端定时调用心跳可同时维持会话列表中的 isOnline 判定(10 分钟内活跃即视为在线)。
  • 令牌仅从 Authorization: Bearer 请求头读取,不接受 URL Query 传参。

本站基于 VitePress 构建,文档内容以 backend-Go handler 源码为权威依据