sumor 3.2.3 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +367 -414
  2. package/README.zh-CN.md +570 -0
  3. package/dist/server/index.d.ts +2 -6
  4. package/dist/server/index.d.ts.map +1 -1
  5. package/dist/server/index.js +1 -5
  6. package/dist/server/index.js.map +1 -1
  7. package/dist/server/middlewares/loadJwtUserMiddleware.d.ts +0 -1
  8. package/dist/server/middlewares/loadJwtUserMiddleware.d.ts.map +1 -1
  9. package/dist/server/middlewares/loadJwtUserMiddleware.js +4 -2
  10. package/dist/server/middlewares/loadJwtUserMiddleware.js.map +1 -1
  11. package/dist/server/mock/mockApiRoutes.d.ts +14 -0
  12. package/dist/server/mock/mockApiRoutes.d.ts.map +1 -0
  13. package/dist/server/mock/mockApiRoutes.js +149 -0
  14. package/dist/server/mock/mockApiRoutes.js.map +1 -0
  15. package/dist/server/mock/mockConfig.d.ts +34 -0
  16. package/dist/server/mock/mockConfig.d.ts.map +1 -0
  17. package/dist/server/mock/mockConfig.js +47 -0
  18. package/dist/server/mock/mockConfig.js.map +1 -0
  19. package/dist/server/mock/mockRoutes.d.ts +9 -0
  20. package/dist/server/mock/mockRoutes.d.ts.map +1 -0
  21. package/dist/server/mock/mockRoutes.js +97 -0
  22. package/dist/server/mock/mockRoutes.js.map +1 -0
  23. package/dist/server/mock/mockTokenUtils.d.ts +30 -0
  24. package/dist/server/mock/mockTokenUtils.d.ts.map +1 -0
  25. package/dist/server/mock/mockTokenUtils.js +81 -0
  26. package/dist/server/mock/mockTokenUtils.js.map +1 -0
  27. package/dist/server/routes.d.ts +1 -0
  28. package/dist/server/routes.d.ts.map +1 -1
  29. package/dist/server/routes.js +29 -25
  30. package/dist/server/routes.js.map +1 -1
  31. package/dist/server/services/oauthService.d.ts +0 -8
  32. package/dist/server/services/oauthService.d.ts.map +1 -1
  33. package/dist/server/services/oauthService.js +0 -24
  34. package/dist/server/services/oauthService.js.map +1 -1
  35. package/dist/server/types/oauth.d.ts +0 -1
  36. package/dist/server/types/oauth.d.ts.map +1 -1
  37. package/dist/server/utils/config.d.ts.map +1 -1
  38. package/dist/server/utils/config.js +13 -0
  39. package/dist/server/utils/config.js.map +1 -1
  40. package/dist/web/OAuthStore.d.ts +11 -5
  41. package/dist/web/OAuthStore.d.ts.map +1 -1
  42. package/dist/web/OAuthStore.js +43 -64
  43. package/dist/web/OAuthStore.js.map +1 -1
  44. package/dist/web/UrlHelper.d.ts +1 -0
  45. package/dist/web/UrlHelper.d.ts.map +1 -1
  46. package/dist/web/UrlHelper.js +11 -0
  47. package/dist/web/UrlHelper.js.map +1 -1
  48. package/dist/web/api/login.d.ts +2 -2
  49. package/dist/web/api/login.d.ts.map +1 -1
  50. package/dist/web/api/login.js +12 -2
  51. package/dist/web/api/login.js.map +1 -1
  52. package/dist/web/api/logout.d.ts +1 -1
  53. package/dist/web/api/logout.d.ts.map +1 -1
  54. package/dist/web/api/logout.js +3 -2
  55. package/dist/web/api/logout.js.map +1 -1
  56. package/package.json +2 -1
  57. package/dist/server/middlewares/isLoggedMiddleware.d.ts +0 -15
  58. package/dist/server/middlewares/isLoggedMiddleware.d.ts.map +0 -1
  59. package/dist/server/middlewares/isLoggedMiddleware.js +0 -35
  60. package/dist/server/middlewares/isLoggedMiddleware.js.map +0 -1
  61. package/dist/server/middlewares/isVerifiedMiddleware.d.ts +0 -16
  62. package/dist/server/middlewares/isVerifiedMiddleware.d.ts.map +0 -1
  63. package/dist/server/middlewares/isVerifiedMiddleware.js +0 -44
  64. package/dist/server/middlewares/isVerifiedMiddleware.js.map +0 -1
@@ -0,0 +1,570 @@
1
+ # Sumor - OAuth 认证框架
2
+
3
+ [![npm version](https://img.shields.io/npm/v/sumor.svg)](https://www.npmjs.com/package/sumor)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Sumor 是一个面向 Express.js 应用的完整 OAuth 2.0 认证框架,内置基于角色的访问控制(RBAC)。它将 OAuth 集成、令牌管理和权限路由保护简化为开箱即用的中间件与工具函数,适用于多服务架构。
7
+
8
+ [English Documentation](https://www.npmjs.com/package/sumor?activeTab=explore&filePath=README.md)
9
+
10
+ ## 核心特性
11
+
12
+ - **完整 OAuth 2.0 流程**:完整的授权码流程,自动令牌交换
13
+ - **会话与令牌管理**:基于 HTTP-only Cookie 的安全令牌刷新机制
14
+ - **JWT 验证**:通过 JWKS(JSON Web Key Set)进行内置 JWT 签名校验
15
+ - **基于角色的访问控制(RBAC)**:应用启动时同步权限定义,运行时进行权限/角色校验
16
+ - **TypeScript 优先**:完整的 TypeScript 支持与类型定义
17
+ - **Express 集成**:即插即用的中间件与预配置路由
18
+ - **Mock 模式**:无需真实 OAuth 服务即可在本地进行开发调试
19
+ - **Web 客户端 SDK**:浏览器端令牌刷新、用户状态管理与权限工具函数
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ npm install sumor
25
+ ```
26
+
27
+ ## 架构概览
28
+
29
+ Sumor 分为两个入口:
30
+
31
+ | 入口 | 用途 |
32
+ | ----------- | --------------------------- |
33
+ | `sumor` | 服务端(Node.js / Express) |
34
+ | `sumor/web` | 客户端(浏览器) |
35
+
36
+ ```
37
+ 浏览器应用
38
+ │ (1) 应用初始化时调用 refreshToken()
39
+ │ (2) login() → 跳转至 OAuth 授权页
40
+ │ (3) OAuth 回调 → /api/oauth/callback
41
+ │ (4) 后续请求携带 HttpOnly Cookie
42
+
43
+ 你的 Express 应用
44
+ ├── oauthRoutes ← /api/oauth/*(令牌刷新、回调、登出)
45
+ ├── loadJwtUserMiddleware ← 校验 JWT,注入 req.jwtUser
46
+ └── 你的业务路由 ← 访问 req.jwtUser,调用 OAuthService 方法
47
+
48
+ OAuth 服务提供商
49
+ └── 签发 JWT,管理用户与权限,提供 JWKS 公钥
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 服务端用法
55
+
56
+ ### 引入
57
+
58
+ ```typescript
59
+ import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
60
+ ```
61
+
62
+ ### 导出说明
63
+
64
+ | 导出 | 类型 | 说明 |
65
+ | ----------------------- | -------------- | ----------------------------- |
66
+ | `OAuthService` | 类 | 与 OAuth 提供商 API 交互 |
67
+ | `loadJwtUserMiddleware` | 中间件 | 校验 JWT 并注入 `req.jwtUser` |
68
+ | `oauthRoutes` | Express Router | 预配置的 OAuth 路由 |
69
+
70
+ ### 基础配置
71
+
72
+ 在 Express 应用中注册 OAuth 路由和中间件:
73
+
74
+ ```typescript
75
+ import express from 'express'
76
+ import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
77
+
78
+ const app = express()
79
+
80
+ // 注册预配置的 OAuth 路由(回调、令牌刷新、登出)
81
+ app.use('/api/oauth', oauthRoutes)
82
+
83
+ // JWT 中间件:校验令牌并为后续路由注入 req.jwtUser
84
+ app.use(loadJwtUserMiddleware)
85
+
86
+ // 你的受保护路由
87
+ app.use('/api/user', userRoutes)
88
+ ```
89
+
90
+ > **注意:** 请在 `loadJwtUserMiddleware` 之前注册 `oauthRoutes`,确保 OAuth 回调端点(`/api/oauth/callback`)无需认证即可访问。
91
+
92
+ ### 启动时同步权限
93
+
94
+ 在应用启动时,将权限定义注册到 OAuth 提供商:
95
+
96
+ ```typescript
97
+ const oauthService = new OAuthService()
98
+
99
+ await oauthService.updatePermissions({
100
+ permissions: ['posts:view', 'posts:create', 'posts:edit', 'posts:delete'],
101
+ permissionLabels: [{ module: 'posts', zh: '文章管理', en: 'Posts Management' }]
102
+ })
103
+ ```
104
+
105
+ 权限字符串格式为 `<模块>:<操作>`。
106
+
107
+ ### 在路由中访问用户信息
108
+
109
+ `loadJwtUserMiddleware` 执行后,`req.jwtUser` 包含解码后的 JWT 载荷:
110
+
111
+ ```typescript
112
+ app.get('/api/profile', (req, res) => {
113
+ const { userId, roles, permissions, isVerified } = req.jwtUser
114
+
115
+ res.json({
116
+ userId,
117
+ roles: roles?.split(',') ?? [],
118
+ permissions: permissions?.split(',') ?? [],
119
+ isVerified: isVerified === 1
120
+ })
121
+ })
122
+ ```
123
+
124
+ **`req.jwtUser` 属性说明:**
125
+
126
+ | 属性 | 类型 | 说明 |
127
+ | ------------- | -------- | ------------------------------ |
128
+ | `userId` | `string` | 用户唯一标识 |
129
+ | `roles` | `string` | 逗号分隔的角色 ID |
130
+ | `permissions` | `string` | 逗号分隔的权限字符串 |
131
+ | `isVerified` | `number` | `1` 表示已认证,`0` 表示未认证 |
132
+ | `tenantId` | `string` | 多租户标识 |
133
+ | `jti` | `string` | JWT ID(会话标识符) |
134
+ | `exp` | `number` | 令牌过期时间戳 |
135
+ | `iat` | `number` | 令牌签发时间戳 |
136
+
137
+ ### OAuthService 方法
138
+
139
+ 在服务端任意位置创建实例,自动读取环境变量配置:
140
+
141
+ ```typescript
142
+ const oauthService = new OAuthService()
143
+ ```
144
+
145
+ #### `updatePermissions(config)`
146
+
147
+ 将应用的权限定义同步到 OAuth 提供商。
148
+
149
+ ```typescript
150
+ await oauthService.updatePermissions({
151
+ permissions: ['resource:view', 'resource:edit'],
152
+ permissionLabels: [{ module: 'resource', zh: '资源管理', en: 'Resource Management' }]
153
+ })
154
+ ```
155
+
156
+ #### `getUserInfo(userId)`
157
+
158
+ 获取单个用户的详细信息。
159
+
160
+ ```typescript
161
+ const userInfo = await oauthService.getUserInfo('user-123')
162
+ // 返回:{ userId, username, nickname, email, avatar, ... }
163
+ ```
164
+
165
+ #### `getUsersInfo(userIds)`
166
+
167
+ 批量获取多个用户的信息。
168
+
169
+ ```typescript
170
+ const users = await oauthService.getUsersInfo(['user-1', 'user-2', 'user-3'])
171
+ // 返回:[{ userId, username, ... }, ...]
172
+ ```
173
+
174
+ #### `searchUsers(searchTerm, limit)`
175
+
176
+ 按名称或邮箱搜索用户。
177
+
178
+ ```typescript
179
+ const results = await oauthService.searchUsers('alice', 20)
180
+ // 返回:[{ userId, username, email, ... }, ...]
181
+ ```
182
+
183
+ #### `revokeSession(sessionId)`
184
+
185
+ 登出时撤销(黑名单化)会话。
186
+
187
+ ```typescript
188
+ await oauthService.revokeSession(req.jwtUser.jti)
189
+ ```
190
+
191
+ #### `checkBlacklist(sessionId)`
192
+
193
+ 检查会话是否已被撤销。
194
+
195
+ ```typescript
196
+ const isRevoked = await oauthService.checkBlacklist(sessionId)
197
+ ```
198
+
199
+ ### 预配置 OAuth 路由
200
+
201
+ `oauthRoutes` 自动注册以下端点:
202
+
203
+ | 方法 | 路径 | 需要认证 | 说明 |
204
+ | ------ | --------------------- | -------- | --------------------------------------- |
205
+ | `GET` | `/api/oauth/callback` | 否 | 使用授权码换取令牌 |
206
+ | `PUT` | `/api/oauth/token` | 否 | 刷新访问令牌,返回用户信息和 OAuth 地址 |
207
+ | `POST` | `/api/oauth/logout` | 是 | 撤销会话并清除 Cookie |
208
+
209
+ ---
210
+
211
+ ## 客户端用法(`sumor/web`)
212
+
213
+ ### 引入
214
+
215
+ ```typescript
216
+ import {
217
+ refreshToken,
218
+ login,
219
+ logout,
220
+ hasPermission,
221
+ hasRole,
222
+ oauthUrl,
223
+ oauthStore,
224
+ axios
225
+ } from 'sumor/web'
226
+ import type { ApiResponse } from 'sumor/web'
227
+ ```
228
+
229
+ ### 导出说明
230
+
231
+ | 导出 | 类型 | 说明 |
232
+ | --------------- | ---------- | ------------------------------------------ |
233
+ | `refreshToken` | 函数 | 刷新令牌并同步用户状态(应用初始化时调用) |
234
+ | `login` | 函数 | 跳转到 OAuth 登录页 |
235
+ | `logout` | 函数 | 调用登出端点并清除用户状态 |
236
+ | `hasPermission` | 函数 | 检查当前用户是否有某个权限 |
237
+ | `hasRole` | 函数 | 检查当前用户是否有某个角色 |
238
+ | `oauthStore` | 单例 | 内存中的用户和 OAuth 状态存储 |
239
+ | `oauthUrl` | 对象 | 生成 OAuth 提供商导航地址的工具函数 |
240
+ | `axios` | Axios 实例 | 预配置 Axios,自动处理 401 令牌刷新重试 |
241
+ | `ApiResponse` | 类型 | 标准 API 响应封装类型 |
242
+
243
+ ### 应用初始化
244
+
245
+ 在应用初始化时调用一次 `refreshToken()`,从已存储的 refresh token Cookie 中恢复用户状态:
246
+
247
+ ```typescript
248
+ // main.ts 或 App.vue 的 onMounted
249
+ import { refreshToken } from 'sumor/web'
250
+
251
+ await refreshToken()
252
+
253
+ // 此后可通过 oauthStore 访问用户状态
254
+ ```
255
+
256
+ > 在 SSR 环境中,`refreshToken()` 会直接返回,不执行任何操作。
257
+
258
+ ### 登录与登出
259
+
260
+ ```typescript
261
+ import { login, logout } from 'sumor/web'
262
+
263
+ // 跳转到 OAuth 授权页
264
+ login()
265
+
266
+ // 登出:撤销会话并清除用户状态
267
+ await logout()
268
+ ```
269
+
270
+ ### 订阅用户状态变化
271
+
272
+ 通过 `oauthStore` 读取当前用户并监听状态变化:
273
+
274
+ ```typescript
275
+ import { oauthStore } from 'sumor/web'
276
+
277
+ // 读取当前用户
278
+ const user = oauthStore.getUser()
279
+ // { id, isVerified, roles, permissions } 或 null
280
+
281
+ // 订阅登录/登出事件
282
+ oauthStore.onUserChange(user => {
283
+ if (user) {
284
+ console.log('已登录:', user.id)
285
+ } else {
286
+ console.log('已登出')
287
+ }
288
+ })
289
+ ```
290
+
291
+ **`UserInfo` 属性说明:**
292
+
293
+ | 属性 | 类型 | 说明 |
294
+ | ------------- | -------- | -------------------- |
295
+ | `id` | `string` | 用户 ID |
296
+ | `isVerified` | `number` | `1` 表示已认证 |
297
+ | `roles` | `string` | 逗号分隔的角色 ID |
298
+ | `permissions` | `string` | 逗号分隔的权限字符串 |
299
+
300
+ ### 权限与角色检查
301
+
302
+ ```typescript
303
+ import { hasPermission, hasRole } from 'sumor/web'
304
+
305
+ // 检查特定权限(模块 + 操作)
306
+ if (hasPermission('posts', 'edit')) {
307
+ // 用户可以编辑文章
308
+ }
309
+
310
+ // 检查模块下的任意权限(通配符)
311
+ if (hasPermission('posts', '*')) {
312
+ // 用户有 posts 模块的任意权限
313
+ }
314
+
315
+ // 检查角色
316
+ if (hasRole('admin')) {
317
+ // 用户是管理员
318
+ }
319
+ ```
320
+
321
+ ### OAuth 地址工具函数
322
+
323
+ ```typescript
324
+ import { oauthUrl } from 'sumor/web'
325
+
326
+ oauthUrl.avatar(userId) // 用户头像图片地址
327
+ oauthUrl.user() // 用户个人中心地址
328
+ oauthUrl.home() // OAuth 提供商首页地址
329
+ oauthUrl.site() // 站点管理页面地址
330
+ oauthUrl.feedback() // 意见反馈页面地址
331
+ ```
332
+
333
+ 在 [Mock 模式](#mock-模式) 下,这些地址会指向本地占位页面。
334
+
335
+ ### 发送带认证的 HTTP 请求
336
+
337
+ 导出的 `axios` 实例会在 401 响应时自动刷新令牌并重试请求:
338
+
339
+ ```typescript
340
+ import { axios } from 'sumor/web'
341
+ import type { ApiResponse } from 'sumor/web'
342
+
343
+ // GET
344
+ const { data } = await axios.get<ApiResponse<UserInfo>>('/api/user/info')
345
+
346
+ // POST
347
+ const { data } = await axios.post<ApiResponse<Item>>('/api/items', { name: '我的项目' })
348
+
349
+ // PUT
350
+ const { data } = await axios.put<ApiResponse<Item>>(`/api/items/${id}`, updates)
351
+
352
+ // DELETE
353
+ const { data } = await axios.delete<ApiResponse<null>>(`/api/items/${id}`)
354
+
355
+ // 带进度的文件上传
356
+ await axios.post<ApiResponse<UploadResult>>('/api/upload', formData, {
357
+ headers: { 'Content-Type': 'multipart/form-data' },
358
+ onUploadProgress: e => {
359
+ const percent = Math.round((e.loaded * 100) / (e.total ?? e.loaded))
360
+ console.log(`上传进度:${percent}%`)
361
+ }
362
+ })
363
+ ```
364
+
365
+ **`ApiResponse<T>` 类型:**
366
+
367
+ ```typescript
368
+ interface ApiResponse<T> {
369
+ code: string // 成功时为 'OK'
370
+ message: string
371
+ data: T
372
+ }
373
+ ```
374
+
375
+ ---
376
+
377
+ ## 环境变量配置
378
+
379
+ Sumor 完全通过环境变量读取配置,无需向构造函数传参。
380
+
381
+ ### 必填环境变量
382
+
383
+ ```bash
384
+ OAUTH_ENDPOINT=https://auth.example.com
385
+ OAUTH_CLIENT_KEY=your-app-client-id
386
+ OAUTH_CLIENT_SECRET=your-app-client-secret
387
+ OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
388
+ ```
389
+
390
+ | 变量 | 说明 |
391
+ | --------------------- | ------------------------------------- |
392
+ | `OAUTH_ENDPOINT` | OAuth 提供商的根地址 |
393
+ | `OAUTH_CLIENT_KEY` | OAuth 应用客户端 ID |
394
+ | `OAUTH_CLIENT_SECRET` | OAuth 应用客户端密钥 |
395
+ | `OAUTH_REDIRECT_URI` | 回调地址(须与 OAuth 提供商配置一致) |
396
+
397
+ JWT 签名校验使用 OAuth 提供商的 JWKS 公钥(从 `{OAUTH_ENDPOINT}/api/oauth/jwks` 获取),无需本地密钥。
398
+
399
+ ---
400
+
401
+ ## Mock 模式
402
+
403
+ Mock 模式允许在没有真实 OAuth 服务的情况下进行本地开发。通过设置 `OAUTH_MOCK=true` 启用。此模式下,Sumor 使用 HS256 签发本地 JWT 令牌,并注册额外的 Mock 专用端点。
404
+
405
+ ### 工作原理
406
+
407
+ 当 `OAUTH_MOCK=true` 时:
408
+
409
+ - `oauthRoutes` 在 `/api/oauth/mock/` 下注册额外的子路由
410
+ - `PUT /api/oauth/token` 路由在本地校验 Mock 令牌,不调用 OAuth 提供商
411
+ - Web 客户端的 `login()` 直接调用 `POST /api/oauth/mock/login`,不跳转到 OAuth 提供商
412
+ - `logout()` 调用 `POST /api/oauth/mock/logout` 在本地清除 Cookie
413
+ - `oauthUrl.*` 工具函数返回本地占位页面地址
414
+
415
+ ### Mock 服务端环境变量
416
+
417
+ ```bash
418
+ # 启用 Mock 模式
419
+ OAUTH_MOCK=true
420
+
421
+ # Mock 用户配置(均为可选,括号内为默认值)
422
+ OAUTH_MOCK_USER_ID=mock-user-001
423
+ OAUTH_MOCK_USER_ROLES=admin
424
+ OAUTH_MOCK_USER_PERMISSIONS=
425
+ OAUTH_MOCK_USER_IS_VERIFIED=1
426
+ ```
427
+
428
+ | 变量 | 默认值 | 说明 |
429
+ | ----------------------------- | --------------- | --------------------------- |
430
+ | `OAUTH_MOCK` | `false` | 设为 `true` 启用 Mock 模式 |
431
+ | `OAUTH_MOCK_USER_ID` | `mock-user-001` | Mock 用户 ID |
432
+ | `OAUTH_MOCK_USER_ROLES` | `admin` | 逗号分隔的 Mock 角色 |
433
+ | `OAUTH_MOCK_USER_PERMISSIONS` | _(空)_ | 逗号分隔的 Mock 权限 |
434
+ | `OAUTH_MOCK_USER_IS_VERIFIED` | `1` | Mock 认证状态(`0` 或 `1`) |
435
+
436
+ ### Mock 专用路由(仅在 `OAUTH_MOCK=true` 时注册)
437
+
438
+ | 方法 | 路径 | 说明 |
439
+ | ------ | ----------------------------- | -------------------------------------------- |
440
+ | `POST` | `/api/oauth/mock/login` | 签发 Mock 令牌并返回用户信息(无需页面跳转) |
441
+ | `POST` | `/api/oauth/mock/logout` | 清除 Mock 令牌 Cookie |
442
+ | `GET` | `/api/oauth/mock/avatar/:id` | 返回 404(Mock 模式无头像服务) |
443
+ | `GET` | `/api/oauth/mock/nav/:target` | OAuth 提供商导航链接的本地占位页面 |
444
+
445
+ ### Mock 模式示例配置
446
+
447
+ **.env.development:**
448
+
449
+ ```bash
450
+ OAUTH_MOCK=true
451
+ OAUTH_MOCK_USER_ID=dev-user-1
452
+ OAUTH_MOCK_USER_ROLES=admin
453
+ OAUTH_MOCK_USER_PERMISSIONS=posts:view,posts:edit,posts:delete
454
+ OAUTH_MOCK_USER_IS_VERIFIED=1
455
+ ```
456
+
457
+ 应用代码无需任何修改 —— 相同的 `oauthRoutes`、`loadJwtUserMiddleware`、`login()` 和 `logout()` 调用在 Mock 模式和生产模式下均正常工作。
458
+
459
+ ### 安全提示
460
+
461
+ Mock 令牌使用固定的 HS256 密钥签名,可通过载荷中的 `iss: 'mock-oauth'` 字段识别。**请勿在生产环境中使用 `OAUTH_MOCK=true`。**
462
+
463
+ ---
464
+
465
+ ## 使用示例
466
+
467
+ ### 服务端 — 路由权限校验
468
+
469
+ ```typescript
470
+ app.post('/api/posts', (req, res) => {
471
+ const permissions = req.jwtUser.permissions?.split(',') ?? []
472
+
473
+ if (!permissions.includes('posts:create')) {
474
+ return res.status(403).json({ code: 'FORBIDDEN', message: '权限不足' })
475
+ }
476
+
477
+ // 创建文章...
478
+ res.json({ code: 'OK', data: { id: 'new-post-id' } })
479
+ })
480
+ ```
481
+
482
+ ### 服务端 — 从 OAuth 提供商获取用户信息
483
+
484
+ ```typescript
485
+ app.get('/api/posts/:id/author', async (req, res) => {
486
+ const post = await db.findPost(req.params.id)
487
+ const author = await oauthService.getUserInfo(post.authorId)
488
+
489
+ res.json({ code: 'OK', data: author })
490
+ })
491
+ ```
492
+
493
+ ### 客户端 — Vue 组件登录/登出
494
+
495
+ ```typescript
496
+ import { login, logout, oauthStore, hasPermission } from 'sumor/web'
497
+ import { ref, onMounted } from 'vue'
498
+
499
+ const user = ref(oauthStore.getUser())
500
+
501
+ onMounted(() => {
502
+ oauthStore.onUserChange(u => {
503
+ user.value = u
504
+ })
505
+ })
506
+
507
+ const canEdit = () => hasPermission('posts', 'edit')
508
+ ```
509
+
510
+ ```html
511
+ <template>
512
+ <button v-if="!user" @click="login">登录</button>
513
+ <button v-else @click="logout">登出</button>
514
+ <button v-if="canEdit()" @click="editPost">编辑</button>
515
+ </template>
516
+ ```
517
+
518
+ ### 客户端 — Vue Router 路由守卫
519
+
520
+ ```typescript
521
+ import { refreshToken, hasPermission } from 'sumor/web'
522
+
523
+ // 挂载路由前恢复用户状态
524
+ await refreshToken()
525
+
526
+ router.beforeEach(to => {
527
+ if (to.meta.requiresPermission) {
528
+ const [module, operation] = (to.meta.requiresPermission as string).split(':')
529
+ if (!hasPermission(module, operation)) {
530
+ return '/403'
531
+ }
532
+ }
533
+ })
534
+ ```
535
+
536
+ ---
537
+
538
+ ## 安全注意事项
539
+
540
+ - 令牌存储在 **HTTP-only Cookie** 中,JavaScript 无法访问(XSS 防护)
541
+ - 生产环境请务必使用 **HTTPS**
542
+ - JWT 签名通过 OAuth 提供商的 JWKS 公钥进行校验
543
+ - 登出操作会立即在 OAuth 提供商侧**将会话加入黑名单**
544
+ - 请勿在非本地开发环境中开启 `OAUTH_MOCK=true`
545
+
546
+ ---
547
+
548
+ ## 常见问题排查
549
+
550
+ ### `req.jwtUser` 为 `undefined`
551
+
552
+ 请确保 `loadJwtUserMiddleware` 在访问 `req.jwtUser` 的路由之前注册。
553
+
554
+ ### 令牌校验失败
555
+
556
+ 请确认 `OAUTH_ENDPOINT` 指向正确的 OAuth 提供商地址。Sumor 从 `{OAUTH_ENDPOINT}/api/oauth/jwks` 获取 JWKS 公钥。
557
+
558
+ ### `refreshToken()` 执行后用户未设置
559
+
560
+ 可能是 refresh token Cookie 不存在或已过期,需要用户重新通过 `login()` 登录。
561
+
562
+ ### Mock 模式下受保护路由返回 401
563
+
564
+ 请确认 `OAUTH_MOCK=true` 在服务器启动时已正确设置。修改该变量后需重启服务。
565
+
566
+ ---
567
+
568
+ ## 许可证
569
+
570
+ MIT License — 详见 [LICENSE](LICENSE)。
@@ -5,15 +5,11 @@
5
5
  import OAuthService from './services/oauthService';
6
6
  import oauthRoutes from './routes';
7
7
  import { loadJwtUserMiddleware } from './middlewares/loadJwtUserMiddleware';
8
- import { isLoggedMiddleware } from './middlewares/isLoggedMiddleware';
9
- import { isVerifiedMiddleware } from './middlewares/isVerifiedMiddleware';
10
8
  /**
11
9
  * 导出所需接口
12
10
  * - OAuthService: OAuth 服务类,用于权限同步和各种 API 调用
13
11
  * - loadJwtUserMiddleware: JWT 用户加载中间件
14
- * - oauthRoutes: OAuth 路由
15
- * - isLoggedMiddleware: 检查用户是否已登录的中间件
16
- * - isVerifiedMiddleware: 检查用户是否已通过认证的中间件
12
+ * - oauthRoutes: OAuth 路由(OAUTH_MOCK=true 时内部已包含 /mock/* 子路由)
17
13
  */
18
- export { OAuthService, loadJwtUserMiddleware, isLoggedMiddleware, isVerifiedMiddleware, oauthRoutes };
14
+ export { OAuthService, loadJwtUserMiddleware, oauthRoutes };
19
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,YAAY,MAAM,yBAAyB,CAAA;AAClD,OAAO,WAAW,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AAEzE;;;;;;;GAOG;AACH,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,WAAW,EACZ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,YAAY,MAAM,yBAAyB,CAAA;AAClD,OAAO,WAAW,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,WAAW,EAAE,CAAA"}
@@ -7,15 +7,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.oauthRoutes = exports.isVerifiedMiddleware = exports.isLoggedMiddleware = exports.loadJwtUserMiddleware = exports.OAuthService = void 0;
10
+ exports.oauthRoutes = exports.loadJwtUserMiddleware = exports.OAuthService = void 0;
11
11
  const oauthService_1 = __importDefault(require("./services/oauthService"));
12
12
  exports.OAuthService = oauthService_1.default;
13
13
  const routes_1 = __importDefault(require("./routes"));
14
14
  exports.oauthRoutes = routes_1.default;
15
15
  const loadJwtUserMiddleware_1 = require("./middlewares/loadJwtUserMiddleware");
16
16
  Object.defineProperty(exports, "loadJwtUserMiddleware", { enumerable: true, get: function () { return loadJwtUserMiddleware_1.loadJwtUserMiddleware; } });
17
- const isLoggedMiddleware_1 = require("./middlewares/isLoggedMiddleware");
18
- Object.defineProperty(exports, "isLoggedMiddleware", { enumerable: true, get: function () { return isLoggedMiddleware_1.isLoggedMiddleware; } });
19
- const isVerifiedMiddleware_1 = require("./middlewares/isVerifiedMiddleware");
20
- Object.defineProperty(exports, "isVerifiedMiddleware", { enumerable: true, get: function () { return isVerifiedMiddleware_1.isVerifiedMiddleware; } });
21
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;AAEH,2EAAkD;AAehD,uBAfK,sBAAY,CAeL;AAdd,sDAAkC;AAkBhC,sBAlBK,gBAAW,CAkBL;AAjBb,+EAA2E;AAczE,sGAdO,6CAAqB,OAcP;AAbvB,yEAAqE;AAcnE,mGAdO,uCAAkB,OAcP;AAbpB,6EAAyE;AAcvE,qGAdO,2CAAoB,OAcP"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;AAEH,2EAAkD;AASzC,uBATF,sBAAY,CASE;AARrB,sDAAkC;AAQY,sBARvC,gBAAW,CAQuC;AAPzD,+EAA2E;AAOpD,sGAPd,6CAAqB,OAOc"}
@@ -12,7 +12,6 @@ import { Response, NextFunction } from 'express';
12
12
  * 注入的 req.jwtUser 包含以下字段:
13
13
  * - userId: 用户 ID(来自 JWT 的 sub 字段)
14
14
  * - jti: JWT 唯一标识
15
- * - roleLevel: 用户角色级别
16
15
  * - isVerified: 用户认证状态标记(0=未认证, 1=已认证)
17
16
  * - roles: 用户角色列表(逗号分隔的字符串)
18
17
  * - permissions: 用户权限列表(逗号分隔的2段式权限字符串)
@@ -1 +1 @@
1
- {"version":3,"file":"loadJwtUserMiddleware.d.ts","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAIhD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED,eAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"loadJwtUserMiddleware.d.ts","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAMhD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED,eAAe,qBAAqB,CAAA"}
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.loadJwtUserMiddleware = loadJwtUserMiddleware;
9
9
  const tokenModel_1 = require("../models/tokenModel");
10
10
  const authUtils_1 = require("../utils/authUtils");
11
+ const mockConfig_1 = require("../mock/mockConfig");
12
+ const mockTokenUtils_1 = require("../mock/mockTokenUtils");
11
13
  /**
12
14
  * JWT 用户加载中间件
13
15
  * 验证 Access Token 并将用户信息注入到 req.jwtUser
@@ -16,7 +18,6 @@ const authUtils_1 = require("../utils/authUtils");
16
18
  * 注入的 req.jwtUser 包含以下字段:
17
19
  * - userId: 用户 ID(来自 JWT 的 sub 字段)
18
20
  * - jti: JWT 唯一标识
19
- * - roleLevel: 用户角色级别
20
21
  * - isVerified: 用户认证状态标记(0=未认证, 1=已认证)
21
22
  * - roles: 用户角色列表(逗号分隔的字符串)
22
23
  * - permissions: 用户权限列表(逗号分隔的2段式权限字符串)
@@ -37,7 +38,8 @@ async function loadJwtUserMiddleware(req, res, next) {
37
38
  }
38
39
  // 2. 验证 Token
39
40
  try {
40
- const claims = await (0, tokenModel_1.verifyToken)(token);
41
+ // Mock 模式下使用 mock token 验证
42
+ const claims = (0, mockConfig_1.isMockMode)() && (0, mockTokenUtils_1.isMockToken)(token) ? (0, mockTokenUtils_1.verifyMockToken)(token) : await (0, tokenModel_1.verifyToken)(token);
41
43
  // 检查 sub(用户ID)是否存在
42
44
  if (!claims.sub) {
43
45
  req.jwtUser = null;
@@ -1 +1 @@
1
- {"version":3,"file":"loadJwtUserMiddleware.js","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAyBH,sDAyCC;AA/DD,qDAAkD;AAClD,kDAAmD;AAEnD;;;;;;;;;;;;;;;;;;GAkBG;AACI,KAAK,UAAU,qBAAqB,CACzC,GAAQ,EACR,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC;QACH,qBAAqB;QACrB,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,cAAc;QACd,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAA,wBAAW,EAAC,KAAK,CAAC,CAAA;YAEvC,mBAAmB;YACnB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBAChB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClB,OAAO,IAAI,EAAE,CAAA;YACf,CAAC;YAED,oBAAoB;YACpB,GAAG,CAAC,OAAO,GAAG;gBACZ,GAAG,MAAM;gBACT,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,sBAAsB;gBAC1C,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAA;YAED,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,wBAAwB;YACxB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;IACH,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,KAAK,CAAC,CAAA;IACb,CAAC;AACH,CAAC;AAED,kBAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"loadJwtUserMiddleware.js","sourceRoot":"","sources":["../../../server/middlewares/loadJwtUserMiddleware.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AA0BH,sDA2CC;AAlED,qDAAkD;AAClD,kDAAmD;AACnD,mDAA+C;AAC/C,2DAAqE;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,qBAAqB,CACzC,GAAQ,EACR,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC;QACH,qBAAqB;QACrB,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,cAAc;QACd,IAAI,CAAC;YACH,2BAA2B;YAC3B,MAAM,MAAM,GACV,IAAA,uBAAU,GAAE,IAAI,IAAA,4BAAW,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAA,gCAAe,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,IAAA,wBAAW,EAAC,KAAK,CAAC,CAAA;YAExF,mBAAmB;YACnB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBAChB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClB,OAAO,IAAI,EAAE,CAAA;YACf,CAAC;YAED,oBAAoB;YACpB,GAAG,CAAC,OAAO,GAAG;gBACZ,GAAG,MAAM;gBACT,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,sBAAsB;gBAC1C,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAA;YAED,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,wBAAwB;YACxB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;IACH,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,GAAG,CAAC,OAAO,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,KAAK,CAAC,CAAA;IACb,CAAC;AACH,CAAC;AAED,kBAAe,qBAAqB,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mock API 路由(挂载到 /api/oauth/mock)
3
+ *
4
+ * 提供给 web SDK 在 mock 模式下直接调用的接口,无需页面跳转。
5
+ *
6
+ * 路由列表:
7
+ * POST /api/oauth/mock/login - 签发 mock token,返回用户信息
8
+ * POST /api/oauth/mock/logout - 清除 mock token cookie
9
+ * GET /api/oauth/mock/avatar/:id - 返回 404(mock 模式无头像服务)
10
+ * GET /api/oauth/mock/nav/:target - 导航占位页(home/site/user/feedback)
11
+ */
12
+ declare const mockApiRoutes: import("express-serve-static-core").Router;
13
+ export default mockApiRoutes;
14
+ //# sourceMappingURL=mockApiRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mockApiRoutes.d.ts","sourceRoot":"","sources":["../../../server/mock/mockApiRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,QAAA,MAAM,aAAa,4CAAmB,CAAA;AA0ItC,eAAe,aAAa,CAAA"}