App JWT Token 颁发与魔术链接登录说明
文档版本:v1.0
更新日期:2026-04-13
适用对象:AOVIS / NEXA App 研发、后端集成、存储桶管理认证接入
目标:说明 App 侧可用的登录换 token 端点、token 生命周期、验证方式,以及后续存储桶管理认证如何复用同一套鉴权逻辑
1. 先说结论
当前站点已经同时具备两条 App 专用 登录换 token 路径:
- Google / Apple 原生 OAuth 身份令牌换 App Token
- 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 登录相关的表:
AppTokenAppMagicToken
含义如下:
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" }
3.2 App Email Magic Link 请求
POST /api/auth/app-magic-link
用途:
- App 内输入邮箱
- 后端生成一次性魔术链接
- 邮件发送给用户
请求体:
{
"email": "user@example.com"
}
成功响应:
{ "sent": true }
失败响应:
400 { "error": "invalid_email" }
说明:
- 不会区分邮箱是否已存在
- 永远返回统一结果,避免邮箱枚举
3.3 App Email Magic Link 兑换 App Token
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. App Magic Link 的用户流程
4.1 用户视角
- 用户在 App 输入邮箱
- App 调用
POST /api/auth/app-magic-link - 用户收到邮件
- 用户点击邮件里的链接
- 如果 App 已安装,Universal Link / App Link 会直接拉起 App
- App 从链接中取出
token - App 调用
POST /api/auth/app-magic-link/verify - 后端返回
access_token - App 用
Bearertoken 调后续 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
然后在服务端:
- 调
verifyAppRequest(req) - 拿到
userId - 再根据业务规则判断这个 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
8.2 Email Magic Link 路径
- 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_tokeninvalid_or_expired_tokeninvalid_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)