跳到主要内容

App JWT Token 颁发与魔术链接登录说明

文档版本:v1.0
更新日期:2026-04-13
适用对象:AOVIS / NEXA App 研发、后端集成、存储桶管理认证接入
目标:说明 App 侧可用的登录换 token 端点、token 生命周期、验证方式,以及后续存储桶管理认证如何复用同一套鉴权逻辑

1. 先说结论

当前站点已经同时具备两条 App 专用 登录换 token 路径:

  1. Google / Apple 原生 OAuth 身份令牌换 App Token
  2. Email Magic Link 换 App Token

这两条路径都不是 Web 端的 Cookie Session 登录。 它们的目标是给原生 App 产出一个可长期携带的 Bearer token,供后续 App API、私有资源访问、以及存储桶管理认证使用。

需要特别说明:

  • 这里的 “App JWT Token” 是产品侧的叫法
  • 实现上并不是自签 JWT
  • 真实实现是 数据库托管的随机 Bearer Token
  • 这样做的目的:
    • 便于撤销
    • 便于落库审计
    • 避免在客户端自行验证签名密钥
    • 便于未来扩展 token 状态、平台、设备名、最后使用时间

2. 当前 App 登录体系概览

2.1 Web 登录与 App 登录是两套不同入口

当前项目中:

  • Web 登录仍然由 Auth.js / NextAuth 提供
  • Web Session 继续走 Cookie
  • App 登录走独立的 App token 端点

也就是说:

  • 不要把 Web 的 /api/auth/* 当成 App 登录 API
  • 不要把 Web Cookie 当成 App 鉴权方式
  • App 端后续请求应使用:
    • Authorization: Bearer <app_token>

2.2 App token 的数据模型

当前数据库里有两张和 App 登录相关的表:

  • AppToken
  • AppMagicToken

含义如下:

  • AppToken
    • 用于 App 已登录后的长期 bearer token
    • 30 天有效
    • 支持撤销、续签、最后使用时间更新
  • AppMagicToken
    • 用于一次性魔术链接 token
    • 10 分钟有效
    • 只能使用一次

3. 端点总览

3.1 Google / Apple 身份令牌换 App Token

POST /api/auth/app-token

用途:

  • App 先在原生侧完成 Google 或 Apple OAuth
  • 拿到 provider 返回的 identity_token
  • 发送给后端换取 AOVIS App Token

请求体:

{
"provider": "google",
"identity_token": "eyJhbGciOi...",
"platform": "ios",
"device_label": "iPhone 15 Pro"
}

或:

{
"provider": "apple",
"identity_token": "eyJhbGciOi...",
"platform": "android",
"device_label": "Pixel 8"
}

成功响应:

{
"access_token": "aovis_app_xxx",
"token_type": "Bearer",
"expires_in": 2592000,
"user_id": "clx123abc456",
"email": "user@example.com"
}

失败响应:

  • 401 { "error": "invalid_token" }

POST /api/auth/app-magic-link

用途:

  • App 内输入邮箱
  • 后端生成一次性魔术链接
  • 邮件发送给用户

请求体:

{
"email": "user@example.com"
}

成功响应:

{ "sent": true }

失败响应:

  • 400 { "error": "invalid_email" }

说明:

  • 不会区分邮箱是否已存在
  • 永远返回统一结果,避免邮箱枚举

POST /api/auth/app-magic-link/verify

用途:

  • App 从 Universal Link / App Link 中拿到 token
  • 把一次性 magic token 发给后端
  • 后端消费 token 并返回 App Token

请求体:

{
"token": "aovis_ml_xxx",
"platform": "ios",
"device_label": "iPhone 15 Pro"
}

成功响应:

{
"access_token": "aovis_app_xxx",
"token_type": "Bearer",
"expires_in": 2592000,
"user_id": "clx123abc456",
"email": "user@example.com"
}

失败响应:

  • 401 { "error": "invalid_or_expired_token" }

3.4 App Token 撤销

DELETE /api/auth/app-token

用途:

  • App 登出
  • 让当前 bearer token 立即失效

请求头:

Authorization: Bearer aovis_app_xxx

成功响应:

{ "revoked": true }

失败响应:

  • 401 { "error": "invalid_token" }

4.1 用户视角

  1. 用户在 App 输入邮箱
  2. App 调用 POST /api/auth/app-magic-link
  3. 用户收到邮件
  4. 用户点击邮件里的链接
  5. 如果 App 已安装,Universal Link / App Link 会直接拉起 App
  6. App 从链接中取出 token
  7. App 调用 POST /api/auth/app-magic-link/verify
  8. 后端返回 access_token
  9. App 用 Bearer token 调后续 API

4.2 链接地址

邮件中的链接固定为:

https://aovis.app/app-auth?token=aovis_ml_xxx

这个地址的设计目的:

  • App 安装时,交给 iOS Universal Links / Android App Links 接管
  • App 未安装时,浏览器显示 fallback 页面

5. Token 规则

5.1 App Token

特点:

  • 前缀:aovis_app_
  • 本体是随机字符串
  • 只保存 SHA-256 hash,不保存明文
  • 30 天有效
  • 可撤销
  • 每次验证成功会更新 lastUsedAt

5.2 App Magic Token

特点:

  • 前缀:aovis_ml_
  • 一次性使用
  • 10 分钟有效
  • 验证成功后会写入 usedAt
  • 已使用或过期会直接失效

6. 后端验证方式

项目里已经提供了一个统一入口:

lib/verify-app-request.ts

导出函数:

verifyAppRequest(req: Request): Promise<string | null>

用途:

  • 从请求头里读取 Authorization: Bearer <token>
  • 校验 App Token
  • 返回 userId
  • 无效时返回 null

6.1 适合哪些场景

这个函数适合用于:

  • App 专用 API
  • 用户自己的私有数据接口
  • 未来的存储桶管理认证
  • App 端文件上传 / 下载 / 索引查询

6.2 使用方式示例

import { verifyAppRequest } from "@/lib/verify-app-request";

export async function GET(req: Request) {
const userId = await verifyAppRequest(req);
if (!userId) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}

return Response.json({ userId });
}

7. 后续存储桶管理认证怎么接

如果后续需要做 App 侧的存储桶管理认证,建议直接复用当前 App Token,不要再发明第二套 token。

7.1 推荐模式

建议所有存储桶相关接口都采用:

Authorization: Bearer aovis_app_xxx

然后在服务端:

  1. verifyAppRequest(req)
  2. 拿到 userId
  3. 再根据业务规则判断这个 user 是否有权限访问目标资源

7.2 典型适用场景

  • App 查询自己的云存储资源
  • App 获取预签名上传地址
  • App 列出自己账号下的对象列表
  • App 读取已授权的媒体文件元数据
  • App 做账户级别的资源管理

7.3 推荐的权限边界

认证通过后,仍然要做授权,不要只看 token。

建议再加一层判断:

  • 当前 userId 是否拥有这个 bucket / object / job 的权限
  • 当前资源是否属于该用户
  • 当前操作是否只读还是写入

也就是说:

  • token 负责证明“你是谁”
  • 业务授权负责证明“你能做什么”

7.4 不建议的做法

不建议:

  • 直接把 bucket 凭证暴露给 App 客户端
  • 让 App 直接持有云厂商 root 级别密钥
  • 在客户端硬编码任何长期有效的存储桶密钥
  • 用 Web Cookie 去控制 App 的存储桶访问

8. App 开发联调清单

8.1 Google / Apple 路径

  • 原生登录拿到 identity_token
  • 请求 POST /api/auth/app-token
  • 保存返回的 access_token
  • 后续请求带上 Authorization: Bearer <access_token>
  • 退出时调用 DELETE /api/auth/app-token
  • App 输入邮箱
  • 请求 POST /api/auth/app-magic-link
  • 用户打开邮件里的链接
  • App 从链接里取出 token
  • 请求 POST /api/auth/app-magic-link/verify
  • 保存返回的 access_token
  • 后续请求带上 Authorization: Bearer <access_token>

8.3 统一错误处理

建议 App 把下面这类错误统一处理成登录失效或重新登录:

  • invalid_token
  • invalid_or_expired_token
  • invalid_email

9. 当前实现边界

当前这套实现只负责:

  • App 身份建立
  • App token 颁发
  • App token 验证
  • App token 撤销
  • App magic link 登录

当前不负责:

  • 设备绑定
  • entitlement 自动发放
  • coupon / discount engine
  • Stripe 充值或退款
  • 存储桶权限模型本身

10. 相关代码位置

  • lib/app-token.ts (legacy codebase reference)
  • app/api/auth/app-token/route.ts (legacy codebase reference)
  • app/api/auth/app-magic-link/route.ts (legacy codebase reference)
  • app/api/auth/app-magic-link/verify/route.ts (legacy codebase reference)
  • lib/verify-app-request.ts (legacy codebase reference)
  • app/app-auth/page.tsx (legacy codebase reference)