Skip to content

Api Authentication

worable edited this page Oct 12, 2026 · 2 revisions

认证方式

StackPanel 有四种认证通道:

通道 凭证 适用
服务端会话 Authorization: Bearer <token> 或 Cookie Web BFF 与后台
平台 API Token Authorization: Bearer sp_... 开放平台 /api/v1、/mcp
OAuth 第三方登录 提供方授权码 用户登录
渠道签名 Ed25519 请求签名头 /sp/v1/* 渠道伙伴

服务端会话

会话是不透明随机串:sess_ 前缀加 32 字节随机数的 base64url 编码。状态存于 Redis, 多副本共享、可即时吊销。请求头或 Cookie 均可携带:

curl -H "Authorization: Bearer <token>" http://127.0.0.1:3001/auth/me

获取 Token

登录与注册由内置 login 插件提供:

POST /login
{ "email": "admin@stackpanel.local", "password": "NewPass123456" }

→ 200 {
  "token": "sess_...",
  "user": { "id": "...", "email": "...", "role": "ADMIN", "status": "ACTIVE" }
}
POST /register
{ "email": "admin@stackpanel.local", "password": "NewPass123456" }

→ 201 { "token": "sess_...", "user": { ... } }

注册要求密码至少 8 位。两个端点都会同时设置会话 Cookie。

Token 说明

  • 形态:sess_ + 32 字节随机数(base64url)。它不是 JWT,服务端不解析载荷。
  • 存储:Redis sp:session:<token>,值为 JSON(userId / createdAt / lastSeenAt); 另有 sp:session:user:<userId> 集合,用于按用户批量吊销。
  • 有效期:SESSION_TTL_SECONDS(默认 43200,即 12 小时)。访问时滑动续期,但两次 续期写入之间至少间隔 60 秒。
  • 可吊销:登出、改密、封禁会立即在各副本生效。
  • 兼容:旧版 HS256 JWT(三段式 token)仍被接受,并在首次带 reply 访问时迁移为 服务端会话、重发 Cookie;无 reply 时按确定性 session id 采用,使重复请求复用 同一会话。相关实现为 apps/api/src/plugins/auth.ts 的 extractSession。

Cookie

登录成功后,API 同时设置 HttpOnly Cookie,默认名 sp_session(SESSION_COOKIE_NAME 可改):

Set-Cookie: sp_session=<token>; HttpOnly; SameSite=Lax; Path=/; Max-Age=43200

Cookie 与 Bearer 使用同一会话 token。extractSession 先取 Bearer 头,其次取 Cookie:

const token = bearer ?? cookie;

Cookie 安全

属性 值 说明
httpOnly true 防 JS 读取
sameSite lax 防 CSRF
secure API_COOKIE_SECURE HTTPS 部署应设为 true
path / 全站有效
maxAge SESSION_TTL_SECONDS 与会话有效期一致

OAuth 第三方登录

流程

1. GET /auth/oauth/providers          → 获取可用提供者
2. GET /auth/oauth/:id/authorize      → 获取授权 URL(重定向用户)
3. POST /auth/oauth/:id/callback      → 交换凭证,获取 token

/authorize 会把 state 写入 Redis(键 oauth:state:<state>)并设置 sp_oauth_state_api Cookie;未传 state 时服务端生成一个并回显。

获取提供者列表

GET /auth/oauth/providers
→ { "providers": [{ "id": "wowid", "name": "WowID" }] }

获取授权 URL

GET /auth/oauth/wowid/authorize?state=random_state&codeChallenge=xxx&nonce=xxx
→ { "authorizeUrl": "https://account.wowcraft.cn/oauth2/authorize?...", "state": "random_state" }

回调

POST /auth/oauth/wowid/callback
{
  "code": "authorization_code",
  "state": "random_state",
  "codeVerifier": "PKCE_verifier",
  "nonce": "nonce_value"
}

→ { "token": "...", "user": { ... } }

身份绑定

  1. 以 providerId + externalId 查 UserIdentity。
  2. 已存在则匹配本地用户。
  3. 不存在则检查 email 冲突;无冲突时创建用户(无密码)并写入身份绑定。

登出

POST /logout
→ 204(吊销会话并清除 Cookie)

会话验证流程

apps/api/src/plugins/auth.ts 的 extractSession:

请求 → extractSession:
  1. 取 Bearer 头,其次 Cookie
  2. 若像平台 API Token(默认 sp_ 前缀,兼容历史 sk-sp- 前缀)→ 解析 ApiToken:
       effectivePermissions = token.scopes ∩ 用户当前权限
  3. 否则查服务端会话(Redis sp:session:<token>):
       命中 → 查 DB user,检查 status === 'ACTIVE',注入 request.user
  4. 否则若是旧 JWT(三段式)→ verifySession 后迁移为服务端会话

用户被禁用(status: DISABLED)后,已有会话在下次请求时即失效。被拒绝的 token 会 进入一个短 TTL 的拒绝缓存,避免对同一无效凭证反复查库。

平台 API Token

自服务端点需要登录会话:

方法 路径 说明
GET /me/api-tokens 列出本人 Token(不返回明文)
GET /me/api-tokens/scopes 可授予的 scope 列表
POST /me/api-tokens 创建;明文仅返回一次
PATCH /me/api-tokens/:id 改名 / 改 scope / 启停 / 改过期
DELETE /me/api-tokens/:id 吊销
GET /me/api-tokens/:id/usage 本人该 Token 的调用审计
  • 明文格式为 sp_ 前缀加 24 字节随机数的 base64url 编码;前缀可用 STACKPANEL_API_TOKEN_PREFIX 覆盖,历史前缀 sk-sp- 继续被接受。库内只存 sha256(明文) 与打码前缀。
  • scope 必须 ⊆ 持有者当前权限。effectivePermissions 是 token scope 与用户当前权限 的交集,因此用户降权会立即收窄其所有 Token。
  • 支持 IP 白名单与过期时间。每 Token RPM(OPEN_API_TOKEN_RPM,默认 240)与并发 上限(OPEN_API_TOKEN_CONCURRENCY,默认 0 表示关闭)在共享状态中计数。
  • lastUsedAt 回写有节流,最多每 5 分钟落库一次。
  • 管理端另有 /admin/api-tokens、/admin/api-tokens/:id/usage 用于审计。

渠道签名(SP v1)

/sp/v1/* 使用 Ed25519 请求签名。签名覆盖方法、路径、时间戳、nonce 与原始请求体; nonce 单次有效,超出签名窗口或重放返回 401,每渠道 RPM 超限返回 429。写操作 (POST)另需 Idempotency-Key。实现位于 apps/api/src/reseller/。

当前用户

GET /auth/me
→ { "user": { "id": "...", "email": "...", "role": "ADMIN", "status": "ACTIVE", "lastLoginAt": "...", "createdAt": "...", "updatedAt": "..." } }

轻量会话探针,供 Web BFF 与其 proxy 作为 /admin 的首层门禁,未登录时返回 { "user": null } 而非 401:

GET /auth/session
→ { "user": { "id": "...", "role": "ADMIN" } }   # 未登录时 { "user": null }

已绑定身份

GET /auth/identities
→ { "identities": [{ "id": "...", "providerId": "wowid", "email": "...", "displayName": "...", "createdAt": "..." }] }

Clone this wiki locally