@scorehub/auth-sdk 1.1.1 → 1.3.0
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.
- package/README.md +394 -0
- package/dist/auth-client.d.ts +14 -0
- package/dist/auth-client.js +41 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# @scorehub/auth-sdk 使用文档
|
|
2
|
+
|
|
3
|
+
## 安装
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @scorehub/auth-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 初始化
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { AuthClient } from '@scorehub/auth-sdk';
|
|
15
|
+
|
|
16
|
+
const authClient = new AuthClient({
|
|
17
|
+
serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1', // Auth-Service 地址
|
|
18
|
+
clientId: 'your-client-id', // OAuth Client ID
|
|
19
|
+
clientSecret: 'your-client-secret', // OAuth Client Secret
|
|
20
|
+
publicKey: fs.readFileSync('./keys/public.pem'), // RS256 公钥 (可选,用于本地验证)
|
|
21
|
+
issuer: 'auth-service', // JWT issuer,默认 'auth-service'
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> `publicKey` 可选。提供时启用本地 JWT 验证(零网络延迟);不提供时需调用 `introspectToken` 远程验证。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 获取公钥
|
|
30
|
+
|
|
31
|
+
服务提供标准 JWKS 端点,可在运行时动态获取公钥。
|
|
32
|
+
|
|
33
|
+
**JWKS 地址:**
|
|
34
|
+
```
|
|
35
|
+
GET https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 动态获取并初始化(推荐)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { AuthClient } from '@scorehub/auth-sdk';
|
|
42
|
+
|
|
43
|
+
async function createAuthClient() {
|
|
44
|
+
const res = await fetch('https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json');
|
|
45
|
+
const { keys } = await res.json();
|
|
46
|
+
const x5c = keys[0].x5c[0];
|
|
47
|
+
|
|
48
|
+
// 还原为 PEM 格式
|
|
49
|
+
const publicKey = [
|
|
50
|
+
'-----BEGIN PUBLIC KEY-----',
|
|
51
|
+
...x5c.match(/.{1,64}/g),
|
|
52
|
+
'-----END PUBLIC KEY-----',
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
return new AuthClient({
|
|
56
|
+
serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1',
|
|
57
|
+
clientId: 'your-client-id',
|
|
58
|
+
clientSecret: 'your-client-secret',
|
|
59
|
+
publicKey,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const authClient = await createAuthClient();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 提前下载保存为文件
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
curl -s https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json \
|
|
70
|
+
| node -e "
|
|
71
|
+
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
72
|
+
const x5c = d.keys[0].x5c[0];
|
|
73
|
+
const pem = '-----BEGIN PUBLIC KEY-----\n' + x5c.match(/.{1,64}/g).join('\n') + '\n-----END PUBLIC KEY-----';
|
|
74
|
+
process.stdout.write(pem);
|
|
75
|
+
" > public.pem
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
然后直接读取文件初始化:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import fs from 'fs';
|
|
82
|
+
|
|
83
|
+
const authClient = new AuthClient({
|
|
84
|
+
serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1',
|
|
85
|
+
clientId: 'your-client-id',
|
|
86
|
+
clientSecret: 'your-client-secret',
|
|
87
|
+
publicKey: fs.readFileSync('./public.pem'),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Token 验证
|
|
94
|
+
|
|
95
|
+
### 本地验证(推荐,需配置 publicKey)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
try {
|
|
99
|
+
const payload = await authClient.verifyAccessToken(token);
|
|
100
|
+
console.log(payload.sub); // 用户 ID
|
|
101
|
+
console.log(payload.username); // 用户名
|
|
102
|
+
console.log(payload.roles); // 角色列表
|
|
103
|
+
console.log(payload.scopes); // 授权范围
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// TokenExpiredError / JsonWebTokenError
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 远程内省(无 publicKey 时使用)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const result = await authClient.introspectToken(token);
|
|
113
|
+
if (result.active) {
|
|
114
|
+
console.log(result.sub, result.username);
|
|
115
|
+
} else {
|
|
116
|
+
// token 已失效
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### TokenPayload 结构
|
|
121
|
+
|
|
122
|
+
| 字段 | 类型 | 说明 |
|
|
123
|
+
|------|------|------|
|
|
124
|
+
| `sub` | string | 用户/Client ID |
|
|
125
|
+
| `type` | `'user'` \| `'client'` | Token 类型 |
|
|
126
|
+
| `username` | string? | 用户名 |
|
|
127
|
+
| `roles` | string[]? | 角色列表 |
|
|
128
|
+
| `clientId` | string? | OAuth Client ID |
|
|
129
|
+
| `scopes` | string[] | 授权范围 |
|
|
130
|
+
| `jti` | string | JWT ID |
|
|
131
|
+
| `iss` | string | 签发者 |
|
|
132
|
+
| `iat` / `exp` | number | 签发/过期时间戳 |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 用户认证
|
|
137
|
+
|
|
138
|
+
### 检查手机号是否已注册
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const { exists } = await authClient.checkUserExists('13800138000');
|
|
142
|
+
if (exists) {
|
|
143
|
+
// 直接走登录流程
|
|
144
|
+
} else {
|
|
145
|
+
// 引导注册
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> 公开接口,无需鉴权。适合在注册/登录前预判手机号状态。
|
|
150
|
+
|
|
151
|
+
### 账号密码登录
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const result = await authClient.loginByPassword('username', 'password');
|
|
155
|
+
console.log(result.accessToken);
|
|
156
|
+
console.log(result.refreshToken);
|
|
157
|
+
console.log(result.user.id, result.user.roles);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 短信验证码登录(含自动注册)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// 先发送验证码
|
|
164
|
+
await authClient.sendSmsCode('13800138000');
|
|
165
|
+
|
|
166
|
+
// 验证码登录(手机号不存在时自动注册)
|
|
167
|
+
const result = await authClient.loginBySms('13800138000', '123456');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 用户注册
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// 先发送验证码
|
|
174
|
+
await authClient.sendSmsCode('13800138000');
|
|
175
|
+
|
|
176
|
+
const result = await authClient.register({
|
|
177
|
+
username: 'testuser',
|
|
178
|
+
password: 'Password123',
|
|
179
|
+
mobile: '13800138000',
|
|
180
|
+
code: '123456',
|
|
181
|
+
realname: '张三', // 可选
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 重置密码
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
await authClient.sendSmsCode('13800138000');
|
|
189
|
+
await authClient.resetPassword('13800138000', '123456', 'NewPassword123');
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 校验短信验证码(一次性)
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
await authClient.verifySmsCode('13800138000', '123456');
|
|
196
|
+
// 成功则继续,失败抛出异常
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Token 管理
|
|
202
|
+
|
|
203
|
+
### 刷新 Token
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const tokenPair = await authClient.refreshToken(refreshToken);
|
|
207
|
+
// { access_token, refresh_token, token_type, expires_in }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 吊销 Token
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
await authClient.revokeToken(accessToken);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 服务间调用
|
|
219
|
+
|
|
220
|
+
### 获取 Service Token(自动缓存)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// client_credentials 方式,内置缓存,剩余有效期 < 60s 时自动刷新
|
|
224
|
+
const serviceToken = await authClient.getServiceToken(['read:users']);
|
|
225
|
+
|
|
226
|
+
fetch('http://other-service/api/data', {
|
|
227
|
+
headers: { Authorization: `Bearer ${serviceToken}` },
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 根据 ID 查询用户信息
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const user = await authClient.getUserById('user-uuid');
|
|
235
|
+
// { sub, username, mobile, email, realname, nickname, avatar, roles }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 管理端操作
|
|
241
|
+
|
|
242
|
+
> 使用 service token 鉴权,无需短信验证码,适合后台管理系统调用。
|
|
243
|
+
|
|
244
|
+
### 管理端创建用户
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const user = await authClient.adminCreateUser({
|
|
248
|
+
username: 'newuser',
|
|
249
|
+
password: 'Password123',
|
|
250
|
+
mobile: '13900139000',
|
|
251
|
+
realname: '李四', // 可选
|
|
252
|
+
});
|
|
253
|
+
// { id, username, mobile }
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 管理端重置用户密码
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
await authClient.adminResetPassword('user-uuid', 'NewPassword123');
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 设置用户状态
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// 将用户状态设为 DISABLED(禁用)
|
|
266
|
+
await authClient.setUserStatus('user-uuid', 'DISABLED');
|
|
267
|
+
|
|
268
|
+
// 将用户状态恢复为 ACTIVE(启用)
|
|
269
|
+
await authClient.setUserStatus('user-uuid', 'ACTIVE');
|
|
270
|
+
|
|
271
|
+
// 将用户状态设为 DELETED(软删除)
|
|
272
|
+
await authClient.setUserStatus('user-uuid', 'DELETED');
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
> 状态枚举:`ACTIVE`(正常)、`DISABLED`(禁用)、`DELETED`(已删除)。
|
|
276
|
+
> 使用 service token 鉴权,适合后台管理系统调用。
|
|
277
|
+
|
|
278
|
+
### 删除用户(软删除)
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
await authClient.deleteUser('user-uuid');
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
> 软删除:将用户状态设为 `DELETED`,保留数据用于审计,不物理删除。等效于 `setUserStatus(userId, 'DELETED')`。
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## 框架中间件
|
|
289
|
+
|
|
290
|
+
### Koa
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import Koa from 'koa';
|
|
294
|
+
import { AuthClient, koaAuthMiddleware } from '@scorehub/auth-sdk';
|
|
295
|
+
|
|
296
|
+
const app = new Koa();
|
|
297
|
+
const authClient = new AuthClient({ /* ... */ });
|
|
298
|
+
|
|
299
|
+
app.use(koaAuthMiddleware(authClient, {
|
|
300
|
+
exclude: ['/health', '/api/v1/auth/login'], // 不需认证的路径
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
// 路由中获取用户信息
|
|
304
|
+
app.use(async (ctx) => {
|
|
305
|
+
const user = ctx.state.user; // TokenPayload
|
|
306
|
+
const admin = ctx.state.admin; // 兼容旧代码: admin._id / admin.username
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Express
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import express from 'express';
|
|
314
|
+
import { AuthClient, expressAuthMiddleware } from '@scorehub/auth-sdk';
|
|
315
|
+
|
|
316
|
+
const app = express();
|
|
317
|
+
const authClient = new AuthClient({ /* ... */ });
|
|
318
|
+
|
|
319
|
+
app.use(expressAuthMiddleware(authClient, {
|
|
320
|
+
exclude: ['/health'],
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
// 路由中获取用户信息
|
|
324
|
+
app.get('/profile', (req, res) => {
|
|
325
|
+
const user = (req as any).user; // TokenPayload
|
|
326
|
+
res.json(user);
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### NestJS
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// app.module.ts
|
|
334
|
+
import { Module } from '@nestjs/common';
|
|
335
|
+
import { AuthClient, AUTH_CLIENT } from '@scorehub/auth-sdk';
|
|
336
|
+
|
|
337
|
+
@Module({
|
|
338
|
+
providers: [
|
|
339
|
+
{
|
|
340
|
+
provide: AUTH_CLIENT,
|
|
341
|
+
useFactory: () => new AuthClient({
|
|
342
|
+
serviceUrl: process.env.AUTH_SERVICE_URL,
|
|
343
|
+
clientId: process.env.AUTH_CLIENT_ID,
|
|
344
|
+
clientSecret: process.env.AUTH_CLIENT_SECRET,
|
|
345
|
+
publicKey: fs.readFileSync('./keys/public.pem'),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
exports: [AUTH_CLIENT],
|
|
350
|
+
})
|
|
351
|
+
export class AppModule {}
|
|
352
|
+
|
|
353
|
+
// controller
|
|
354
|
+
import { UseGuards } from '@nestjs/common';
|
|
355
|
+
import { AuthGuard } from '@scorehub/auth-sdk';
|
|
356
|
+
|
|
357
|
+
@UseGuards(AuthGuard)
|
|
358
|
+
@Controller('users')
|
|
359
|
+
export class UsersController {
|
|
360
|
+
@Get('me')
|
|
361
|
+
getProfile(@Request() req) {
|
|
362
|
+
return req.user; // TokenPayload
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Token 提取规则
|
|
370
|
+
|
|
371
|
+
中间件按以下优先级提取 Token:
|
|
372
|
+
|
|
373
|
+
| 优先级 | 来源 |
|
|
374
|
+
|--------|------|
|
|
375
|
+
| 1 | `Authorization: Bearer <token>` |
|
|
376
|
+
| 2 | `x-access-token` Header |
|
|
377
|
+
| 3 | `access-token` Header(仅 Koa)|
|
|
378
|
+
| 4 | `access_token` Header(仅 Koa)|
|
|
379
|
+
| 5 | `?token=` Query 参数 |
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 错误处理
|
|
384
|
+
|
|
385
|
+
所有方法在失败时抛出带 `authCode` 属性的 Error:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
try {
|
|
389
|
+
await authClient.loginByPassword(username, password);
|
|
390
|
+
} catch (err: any) {
|
|
391
|
+
console.log(err.message); // 错误描述
|
|
392
|
+
console.log(err.authCode); // 业务错误码,如 1001 (Unauthorized)
|
|
393
|
+
}
|
|
394
|
+
```
|
package/dist/auth-client.d.ts
CHANGED
|
@@ -145,8 +145,22 @@ export declare class AuthClient {
|
|
|
145
145
|
* 管理端重置用户密码 (service token 鉴权,无需短信验证码)
|
|
146
146
|
*/
|
|
147
147
|
adminResetPassword(userId: string, newPassword: string): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* 检查手机号是否已注册
|
|
150
|
+
*/
|
|
151
|
+
checkUserExists(mobile: string): Promise<{
|
|
152
|
+
exists: boolean;
|
|
153
|
+
}>;
|
|
148
154
|
/**
|
|
149
155
|
* 发送短信验证码
|
|
150
156
|
*/
|
|
151
157
|
sendSmsCode(mobile: string, type?: number): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* 更新用户状态 (service token 鉴权)
|
|
160
|
+
*/
|
|
161
|
+
setUserStatus(userId: string, status: 'ACTIVE' | 'DISABLED' | 'DELETED'): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* 软删除用户 (将状态设为 DELETED,保留数据用于审计)
|
|
164
|
+
*/
|
|
165
|
+
deleteUser(userId: string): Promise<void>;
|
|
152
166
|
}
|
package/dist/auth-client.js
CHANGED
|
@@ -277,6 +277,21 @@ class AuthClient {
|
|
|
277
277
|
throw err;
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* 检查手机号是否已注册
|
|
282
|
+
*/
|
|
283
|
+
async checkUserExists(mobile) {
|
|
284
|
+
const url = new URL(`${this.serviceUrl}/users/exists`);
|
|
285
|
+
url.searchParams.set('mobile', mobile);
|
|
286
|
+
const response = await fetch(url.toString());
|
|
287
|
+
const result = await response.json();
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
const err = new Error(result.message || 'Check user exists failed');
|
|
290
|
+
err.authCode = result.code;
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
return result.data;
|
|
294
|
+
}
|
|
280
295
|
/**
|
|
281
296
|
* 发送短信验证码
|
|
282
297
|
*/
|
|
@@ -293,5 +308,31 @@ class AuthClient {
|
|
|
293
308
|
throw err;
|
|
294
309
|
}
|
|
295
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* 更新用户状态 (service token 鉴权)
|
|
313
|
+
*/
|
|
314
|
+
async setUserStatus(userId, status) {
|
|
315
|
+
const token = await this.getServiceToken();
|
|
316
|
+
const response = await fetch(`${this.serviceUrl}/users/${userId}/status`, {
|
|
317
|
+
method: 'PATCH',
|
|
318
|
+
headers: {
|
|
319
|
+
'Content-Type': 'application/json',
|
|
320
|
+
Authorization: `Bearer ${token}`,
|
|
321
|
+
},
|
|
322
|
+
body: JSON.stringify({ status }),
|
|
323
|
+
});
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
const result = await response.json();
|
|
326
|
+
const err = new Error(result.message || 'Set user status failed');
|
|
327
|
+
err.authCode = result.code;
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 软删除用户 (将状态设为 DELETED,保留数据用于审计)
|
|
333
|
+
*/
|
|
334
|
+
async deleteUser(userId) {
|
|
335
|
+
return this.setUserStatus(userId, 'DELETED');
|
|
336
|
+
}
|
|
296
337
|
}
|
|
297
338
|
exports.AuthClient = AuthClient;
|