认证技术栈
| 层级 | 技术 | 用途 |
|---|---|---|
| 认证提供商 | Clerk | 登录(邮箱、社交登录)、会话管理、托管 UI |
| JWT 验证 | jose + Clerk JWKS | 服务端 Bearer Token 验证(无需往返请求) |
| Convex 集成 | Clerk JWT 模板(convex) | 使用 applicationID: "convex" 的 Convex 认证 |
| 认证状态 | auth-state.ts | 响应式浏览器认证状态、角色缓存 |
关键文件
| 文件 | 用途 |
|---|---|
convex/auth.config.ts | Convex 认证提供商配置 — Clerk JWT 签发方 + applicationID |
src/services/clerk.ts | Clerk 实例初始化,用于 Convex JWT 模板的 getClerkToken() |
src/services/auth-state.ts | 响应式认证状态、角色获取、会话水合 |
src/components/AuthHeaderWidget.ts | 头部登录按钮、Clerk UserButton |
server/auth-session.ts | 使用 jose + 缓存 JWKS 的服务端 JWT 验证 |
面板门控
高级面板在用户满足访问要求之前,会显示 CTA 遮罩层而非内容。门控原因
| 原因 | 用户所见 | 解决方式 |
|---|---|---|
ANONYMOUS | ”登录以解锁” | 通过 Clerk 登录 |
FREE_TIER | ”升级到 Pro” | 升级订阅 |
NONE | 正常面板内容 | 已解锁 |
如何配置哪些面板为高级面板
三个文件控制门控。添加或移除高级面板时,三者必须保持同步。1. 面板配置 — src/config/panels.ts
在相关变体的面板条目中添加 premium: 'locked':
2. 客户端门控集合 — src/app/panel-layout.ts
将面板键添加到 WEB_PREMIUM_PANELS:
3. 服务端 API 强制执行(如果面板调用高级 API)
客户端 Token 注入 —src/services/runtime.ts(WEB_PREMIUM_API_PATHS):
Authorization: Bearer <token>。
服务端网关 — server/gateway.ts(PREMIUM_RPC_PATHS):
session.role === 'pro'。如果用户不是 pro,则返回 403。
当前受门控的面板
| 面板 | 变体 | 门控类型 |
|---|---|---|
stock-analysis | full, finance | locked(Web) |
stock-backtest | full, finance | locked(Web) |
daily-market-brief | full, finance | locked(Web) |
桌面端行为
在 Tauri 钥匙串中拥有有效WORLDMONITOR_API_KEY 的桌面端用户可绕过所有面板门控。现有的 API 密钥流程不受影响 — Bearer Token 是第二条认证路径,而非替代方案。
服务端会话强制执行
Vercel API 网关为高级端点接受两种形式的身份验证:- 静态 API 密钥 —
X-WorldMonitor-Key头(现有流程,未变更) - Bearer Token —
Authorization: Bearer <clerk_jwt>(用于 Web 用户)
server/auth-session.ts 进行本地 JWKS 验证的 Bearer Token 校验。JWT 验证依据:
- 签发方:
CLERK_JWT_ISSUER_DOMAIN - 受众:
convex(与 Clerk JWT 模板匹配) - 签名:通过 Clerk 发布的 JWKS 使用 RSA256
环境变量
| 变量 | 位置 | 用途 |
|---|---|---|
CLERK_JWT_ISSUER_DOMAIN | Convex + Vercel | 用于 JWT 验证的 Clerk 签发方域名 |
VITE_CLERK_PUBLISHABLE_KEY | Vercel | 客户端 Clerk 发布密钥 |
用户角色
用户角色(pro / free)存储为 Clerk JWT 中的 plan 声明。服务端从已验证的 Token 负载中提取此值。未知或缺失的 plan 值默认为 free(安全失败 — 绝不会默认为 pro)。
在客户端,getAuthState().user?.role 暴露该角色。isProUser() 和 hasPremiumAccess() 都会检查此值以及旧版 API 密钥门控。