File size: 5,570 Bytes
d9ffd67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
---
title: "身份验证与面板门控"
description: "World Monitor 中用户身份验证、高级面板门控与服务端会话强制执行机制的完整技术说明:涵盖 Clerk 集成、加密 Cookie 会话、JWT 令牌校验、权益检查、多设备登录与 Pro 订阅门控逻辑,确保仪表盘、API 与桌面应用的访问控制稳固可靠。"
---

WorldMonitor 使用 [Clerk](https://clerk.com) 进行身份验证。认证系统将高级面板置于登录与层级检查之后,并通过本地 JWT 验证在服务端 API 端点上强制执行基于会话的访问控制。

---

## 认证技术栈

| 层级 | 技术 | 用途 |
|-------|-----------|---------|
| 认证提供商 | 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'`:

```ts
// In FULL_PANELS, FINANCE_PANELS, etc.
'my-panel': { name: 'My Panel', enabled: true, premium: 'locked' }
```

#### 2. 客户端门控集合 — `src/app/panel-layout.ts`

将面板键添加到 `WEB_PREMIUM_PANELS`:

```ts
const WEB_PREMIUM_PANELS = new Set([
  'stock-analysis',
  'stock-backtest',
  'daily-market-brief',
  'my-panel',  // <-- add here
]);
```

此集合驱动响应式 UI 门控 — 当认证状态变化时,该集合中的面板会被检查,并相应地显示/隐藏 CTA。

#### 3. 服务端 API 强制执行(如果面板调用高级 API)

**客户端 Token 注入** — `src/services/runtime.ts`(`WEB_PREMIUM_API_PATHS`):

```ts
const WEB_PREMIUM_API_PATHS = new Set([
  '/api/market/v1/analyze-stock',
  '/api/market/v1/get-stock-analysis-history',
  '/api/market/v1/backtest-stock',
  '/api/market/v1/list-stored-stock-backtests',
  '/api/my-domain/v1/my-endpoint',  // <-- add here
]);
```

当 fetch 请求匹配此集合中的路径且用户拥有 Clerk 会话时,客户端会自动注入 `Authorization: Bearer <token>`。

**服务端网关** — `server/gateway.ts`(`PREMIUM_RPC_PATHS`):

```ts
const PREMIUM_RPC_PATHS = new Set([
  '/api/market/v1/analyze-stock',
  '/api/market/v1/get-stock-analysis-history',
  '/api/market/v1/backtest-stock',
  '/api/market/v1/list-stored-stock-backtests',
  '/api/my-domain/v1/my-endpoint',  // <-- add here
]);
```

网关通过本地 JWKS 验证(jose)校验 Bearer Token,并检查 `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 网关为高级端点接受两种形式的身份验证:

1. **静态 API 密钥** — `X-WorldMonitor-Key` 头(现有流程,未变更)
2. **Bearer Token** — `Authorization: Bearer <clerk_jwt>`(用于 Web 用户)

网关首先尝试 API 密钥。如果在高级端点上失败,则回退到使用 `server/auth-session.ts` 进行本地 JWKS 验证的 Bearer Token 校验。JWT 验证依据:
- **签发方**:`CLERK_JWT_ISSUER_DOMAIN`
- **受众**:`convex`(与 Clerk JWT 模板匹配)
- **签名**:通过 Clerk 发布的 JWKS 使用 RSA256

非高级端点不需要来自 Web 来源的任何身份验证。

---

## 环境变量

| 变量 | 位置 | 用途 |
|----------|-------|---------|
| `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 密钥门控。