aid-npm-test987-sso-sdk 0.1.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.
package/README.md ADDED
@@ -0,0 +1,725 @@
1
+ # aid-sso-sdk
2
+
3
+ AidLux OIDC SSO 前端接入 SDK,面向浏览器环境,和具体前端技术栈无关。
4
+
5
+ SDK 只封装各子系统接入 SSO 时稳定、公共的前端逻辑:
6
+
7
+ - 判断 session cookie 是否存在
8
+ - 构造 `/api/oidc/*` 登录、注册、绑定跳转地址
9
+ - 使用原生 `fetch` 调用 OIDC 原始接口
10
+ - 登出、401 清理、AidLux 个人中心地址构造
11
+ - 账号绑定流程参数拼装
12
+ - 未授权状态码和业务 code 的公共判断
13
+
14
+ SDK 不依赖 Vue、React、Next.js、Umi、Pinia、Redux、Axios,也不使用任何子系统自己的 request 封装。
15
+
16
+ ## 适用范围
17
+
18
+ 适合这些子系统:
19
+
20
+ - 使用 cookie session 接入 AidLux OIDC SSO
21
+ - 后端已提供统一 `/api/oidc/*` 接口
22
+ - 前端需要兼容 hash 路由、history 路由、有本地登录页或无本地登录页
23
+ - 需要统一处理登出、401、个人中心、旧账号绑定
24
+
25
+ SDK 不负责这些业务逻辑:
26
+
27
+ - 子系统自己的用户信息接口,例如 `/api/user/info`
28
+ - 菜单权限、角色权限、账号激活状态
29
+ - Pinia、Redux、Umi initialState 等状态管理细节
30
+ - 本地登录页的 UI 和路由跳转策略
31
+ - 离线私有化 token 模式
32
+ - 子系统业务 request 的完整封装
33
+
34
+ ## 接入总览
35
+
36
+ 一个新子系统建议按下面顺序接入:
37
+
38
+ 1. 后端提供固定 `/api/oidc/*` 接口。
39
+ 2. 后端设置前端可读的 session cookie。
40
+ 3. 前端安装 `aid-sso-sdk`。
41
+ 4. 创建子系统自己的 `ssoSdk` 单例。
42
+ 5. 配置业务请求携带 cookie。
43
+ 6. 在路由守卫或应用初始化里判断登录态。
44
+ 7. 根据产品形态选择“直接跳 SSO”或“先进入本地登录页”。
45
+ 8. 登录页或登录按钮调用 SDK 跳转 SSO。
46
+ 9. 注册入口调用 SDK 跳转 SSO 注册。
47
+ 10. 旧账号迁移场景调用 SDK 绑定流程。
48
+ 11. 业务请求拦截器使用 SDK 判断 401 并清理状态。
49
+ 12. 用户菜单接入登出和 AidLux 个人中心。
50
+ 13. 按验收清单验证完整流程。
51
+
52
+ ## 1. 后端接口准备
53
+
54
+ 所有子系统后端必须提供固定 OIDC 接口:
55
+
56
+ ```text
57
+ GET /api/oidc/login
58
+ GET /api/oidc/register
59
+ GET /api/oidc/callback
60
+ POST /api/oidc/logout
61
+ GET /api/oidc/aidauth
62
+ ```
63
+
64
+ 接口职责:
65
+
66
+ - `/api/oidc/login`:接收 `return_path` 和可选 `extra_param`,然后 302 到 SSO 登录。
67
+ - `/api/oidc/register`:接收 `return_path` 和可选 `extra_param`,然后进入 SSO 注册流程。
68
+ - `/api/oidc/callback`:由 OIDC Provider 回调,后端完成 code 换 token、创建 session、写 cookie,然后 302 回 `return_path`。
69
+ - `/api/oidc/logout`:清理服务端 session,并通过 `Set-Cookie: Max-Age=0` 清理 cookie。
70
+ - `/api/oidc/aidauth`:返回 AidLux 登录中心地址,用于前端构造个人中心 URL。
71
+
72
+ 前端 SDK 不处理 OIDC 的 client id、client secret、authorize、token 交换等后端细节。
73
+
74
+ ## 2. Cookie 约定
75
+
76
+ 每个子系统都需要确定自己的 session cookie 名称:
77
+
78
+ ```ts
79
+ const COOKIE_NAME = 'your_system_session';
80
+ ```
81
+
82
+ 要求:
83
+
84
+ - cookie 由子系统后端设置。
85
+ - cookie 名称不要和其他子系统冲突。
86
+ - cookie 必须允许前端读取,不能设置 `HttpOnly`。
87
+ - 前端只判断 cookie 是否存在,不解析 cookie 值。
88
+ - 业务请求通过浏览器自动携带 cookie 完成鉴权。
89
+
90
+ 已有示例:
91
+
92
+ ```text
93
+ aimo -> aimo_session
94
+ license-manger -> ls_session
95
+ ```
96
+
97
+ ## 3. 安装 SDK
98
+
99
+ 开发阶段通过本地目录依赖安装:
100
+
101
+ ```bash
102
+ pnpm add file:../aid-sso-sdk
103
+ ```
104
+
105
+ 发布到 npm 源后:
106
+
107
+ ```bash
108
+ npm install aid-sso-sdk
109
+ ```
110
+
111
+ 如果项目使用 pnpm,建议保持锁文件只使用 `pnpm-lock.yaml`,不要同时维护多种 lockfile。
112
+
113
+ ## 4. 创建 SSO SDK
114
+
115
+ 建议在子系统里新建一个独立文件:
116
+
117
+ ```text
118
+ src/utils/sso.ts
119
+ ```
120
+
121
+ 示例:
122
+
123
+ ```ts
124
+ import { createSsoSdk } from 'aid-sso-sdk';
125
+
126
+ export const ssoSdk = createSsoSdk({
127
+ cookieName: 'your_system_session',
128
+ from: 'aimo',
129
+ clearLocalAuth() {
130
+ localStorage.removeItem('access_token');
131
+ localStorage.removeItem('refresh_token');
132
+ localStorage.removeItem('user');
133
+ },
134
+ });
135
+ ```
136
+
137
+ 配置说明:
138
+
139
+ ```ts
140
+ type SsoSdkOptions = {
141
+ cookieName: string;
142
+ oidcBasePath?: '/api/oidc';
143
+ from?: 'aimo' | 'aic';
144
+ oidcServerAddress?: string | (() => string);
145
+ navigate?: (url: string) => void;
146
+ clearLocalAuth?: () => void;
147
+ };
148
+ ```
149
+
150
+ 字段说明:
151
+
152
+ - `cookieName`:必填,必须和后端写入的 session cookie 名一致。
153
+ - `oidcBasePath`:默认 `/api/oidc`,当前规范固定为该值。
154
+ - `from`:来源子系统标识。当前 SDK 已固定为 `'aimo' | 'aic'`,新增来源前需要先确认 SSO 是否支持。
155
+ - `oidcServerAddress`:子系统前端访问地址,默认 `window.location.origin`。
156
+ - `navigate`:自定义跳转函数,默认 `window.location.assign(url)`。
157
+ - `clearLocalAuth`:清理子系统自己的本地登录状态,SDK 会在登出和 401 处理中调用。
158
+
159
+ `from` 和 `oidcServerAddress` 是配套参数。只要配置了 `from`,SDK 就会在 `extra_param` 中配套传递 `oidc_server_address`;如果调用方没有显式传 `oidcServerAddress`,SDK 默认使用 `window.location.origin`。
160
+
161
+ 这两个参数主要用于注册流程:SSO 可以根据来源子系统和子系统访问地址,拉取或填写对应子系统的申请试用表单信息。
162
+
163
+ ## 5. 业务代码直接使用 ssoSdk
164
+
165
+ SDK 创建的 `ssoSdk` 实例本身就是一个语义清晰的封装。业务代码直接导入使用,不需要再给每个方法包一层空函数:
166
+
167
+ ```ts
168
+ import { ssoSdk } from '@/utils/sso';
169
+
170
+ // 判断登录态
171
+ if (!ssoSdk.hasSession()) {
172
+ ssoSdk.redirectToLogin();
173
+ }
174
+
175
+ // 登出
176
+ await ssoSdk.logout();
177
+ ```
178
+
179
+ `ssoSdk.hasSession()` 一眼就能看出它来自 SDK;包一层 `hasOidcSession()` 反而多了一层间接,需要跳转到包装文件才能定位实现。
180
+
181
+ ### 什么时候才值得包装
182
+
183
+ 只有一种情况:你需要**改变 SDK 的默认行为**——比如追加子系统特有的默认参数,或者在 SDK 方法前后执行额外的逻辑。
184
+
185
+ ```ts
186
+ // 场景 1:带上项目特有的 extraParam
187
+ export const redirectToLogin = (returnPath?: string) =>
188
+ ssoSdk.redirectToLogin({
189
+ returnPath,
190
+ extraParam: { source: 'license_page' },
191
+ });
192
+
193
+ // 场景 2:退出时额外清理子系统缓存
194
+ export const logout = async () => {
195
+ clearLocalCache();
196
+ await ssoSdk.logout();
197
+ };
198
+ ```
199
+
200
+ 空包装——函数体只有一个 SDK 方法调用、不加任何额外逻辑——不需要写。让业务代码直接用 `ssoSdk` 即可。
201
+
202
+ ## 6. 配置业务请求携带 Cookie
203
+
204
+ 在线 OIDC 模式下,业务请求必须允许携带 cookie。
205
+
206
+ Axios 示例:
207
+
208
+ ```ts
209
+ axios.create({
210
+ baseURL: '/api',
211
+ withCredentials: true,
212
+ });
213
+ ```
214
+
215
+ Umi request 示例:
216
+
217
+ ```ts
218
+ const request = extend({
219
+ prefix: '/api',
220
+ credentials: 'include',
221
+ });
222
+ ```
223
+
224
+ 注意事项:
225
+
226
+ - 在线 OIDC 模式下,不要再给业务请求手动注入 `Authorization`。
227
+ - 鉴权凭证由浏览器自动携带 session cookie。
228
+ - 如果项目同时存在离线 token 模式,离线逻辑保留在子系统内部,不放进 SDK。
229
+
230
+ ## 7. 选择登录入口模式
231
+
232
+ 子系统通常有两种登录入口形态。
233
+
234
+ ### 7.1 无本地登录页:直接跳 SSO
235
+
236
+ 适合没有自己登录页的系统,例如 `license-manger`。
237
+
238
+ 路由守卫、页面切换或应用初始化时:
239
+
240
+ ```ts
241
+ if (!ssoSdk.hasSession()) {
242
+ ssoSdk.redirectToLogin({
243
+ returnPath: window.location.href,
244
+ });
245
+ }
246
+ ```
247
+
248
+ 如果不传 `returnPath`,SDK 默认使用 `window.location.href`:
249
+
250
+ ```ts
251
+ ssoSdk.redirectToLogin();
252
+ ```
253
+
254
+ ### 7.2 有本地登录页:先进入本地 `/login`
255
+
256
+ 适合需要保留本地登录壳、账号密码登录、注册入口、绑定提示的系统,例如 `aimo`。
257
+
258
+ 路由守卫仍由子系统自己控制:
259
+
260
+ ```ts
261
+ router.push(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
262
+ ```
263
+
264
+ 登录页读取 `redirect`,计算最终 SSO 回跳地址:
265
+
266
+ ```ts
267
+ const redirect = route.query.redirect as string | undefined;
268
+ const returnPath = `${window.location.origin}/#${redirect || '/'}`;
269
+
270
+ ssoSdk.redirectToLogin({ returnPath });
271
+ ```
272
+
273
+ 注意:
274
+
275
+ - SDK 不决定是否先进入子系统 `/login`。
276
+ - SDK 只负责最后跳 `/api/oidc/login`。
277
+ - hash 路由、history 路由、本地登录页 redirect 参数由子系统自己计算。
278
+
279
+ ## 8. return_path 规则
280
+
281
+ SDK 的 `return_path` 规则只有两条,由 `getLoginUrl()`、`redirectToLogin()`、`getRegisterUrl()`、`redirectToRegister()` 等方法内部处理:
282
+
283
+ - 传了 `returnPath`:原样使用。
284
+ - 没传:使用 `window.location.href`。
285
+
286
+ 常见写法:
287
+
288
+ history 路由当前页直接跳:
289
+
290
+ ```ts
291
+ ssoSdk.redirectToLogin();
292
+ ```
293
+
294
+ hash 路由在本地登录页里跳:
295
+
296
+ ```ts
297
+ const returnPath = `${window.location.origin}/#${redirect || '/'}`;
298
+ ssoSdk.redirectToLogin({ returnPath });
299
+ ```
300
+
301
+ 不要让 SDK 猜测路由模式。子系统最清楚自己的路由结构,应主动传入准确的 `returnPath`。
302
+
303
+ ## 9. 登录和注册
304
+
305
+ 构造登录地址但不跳转:
306
+
307
+ ```ts
308
+ const loginUrl = ssoSdk.getLoginUrl({ returnPath });
309
+ ```
310
+
311
+ 直接跳转登录:
312
+
313
+ ```ts
314
+ ssoSdk.redirectToLogin({ returnPath });
315
+ ```
316
+
317
+ 构造注册地址但不跳转:
318
+
319
+ ```ts
320
+ const registerUrl = ssoSdk.getRegisterUrl({ returnPath });
321
+ ```
322
+
323
+ 直接跳转注册:
324
+
325
+ ```ts
326
+ ssoSdk.redirectToRegister({ returnPath });
327
+ ```
328
+
329
+ 登录和注册使用相同的 `return_path` 和 `extra_param` 规则。
330
+
331
+ ## 10. 旧账号绑定流程
332
+
333
+ 绑定流程分为两段:
334
+
335
+ 1. 跳转 SSO 前:子系统调用 `redirectToBind()`,把绑定所需参数带到 SSO。
336
+ 2. SSO 登录并回到子系统后:子系统在路由守卫中判断 URL query 里的 `hash`,调用自己的绑定接口完成绑定。
337
+
338
+ SDK 只负责第一段“跳转并携带参数”。第二段“回来后调用子系统绑定接口”必须由子系统实现,因为绑定接口属于各子系统后端。
339
+
340
+ 当子系统旧账号登录后,后端返回需要绑定 AidLux 账号时,跳转前调用:
341
+
342
+ ```ts
343
+ ssoSdk.redirectToBind({
344
+ hash,
345
+ username,
346
+ returnPath,
347
+ });
348
+ ```
349
+
350
+ SDK 会自动处理:
351
+
352
+ - `extra_param.hash`
353
+ - `extra_param.username`
354
+ - `extra_param.bind_hash`
355
+ - `extra_param.from`
356
+ - `extra_param.oidc_server_address`
357
+ - 顶层 query `bind_hash`
358
+
359
+ 如果后端返回了独立的 `bindHash`:
360
+
361
+ ```ts
362
+ ssoSdk.redirectToBind({
363
+ hash,
364
+ bindHash,
365
+ username,
366
+ returnPath,
367
+ });
368
+ ```
369
+
370
+ 如果还有额外绑定参数:
371
+
372
+ ```ts
373
+ ssoSdk.redirectToBind({
374
+ hash,
375
+ username,
376
+ returnPath,
377
+ extraParam: {
378
+ source: 'migration',
379
+ },
380
+ });
381
+ ```
382
+
383
+ `redirectToBind()` 最终会跳转到:
384
+
385
+ ```text
386
+ /api/oidc/login?return_path=...&bind_hash=...&extra_param=...
387
+ ```
388
+
389
+ SSO 登录完成后,子系统后端 callback 会把 `extra_param` 里的 `hash` 带回子系统前端路由。子系统需要在路由守卫中处理:
390
+
391
+ ```ts
392
+ router.beforeEach(async (to, _, next) => {
393
+ const query = to.query;
394
+
395
+ if (query?.hash) {
396
+ try {
397
+ await userBind({ hash: query.hash as string });
398
+ // 绑定成功后移除 hash,避免刷新或再次进入路由时重复绑定
399
+ to.query = omit(to.query, ['hash']);
400
+ next(to);
401
+ return;
402
+ } catch (error) {
403
+ // 绑定失败时应清理 hash,避免登录/绑定循环
404
+ to.query = omit(to.query, ['hash', 'redirect']);
405
+ next('/login');
406
+ return;
407
+ }
408
+ }
409
+
410
+ next();
411
+ });
412
+ ```
413
+
414
+ `aimo` 当前就是这种处理方式:在 `src/router/index.ts` 中判断 `query.hash`,然后调用 `userBind({ hash })`。离线 token 回调场景下,也是在处理 `access_token` 后继续判断 `query.hash` 并调用同一个绑定接口。
415
+
416
+ 注意事项:
417
+
418
+ - `redirectToBind()` 不会调用子系统绑定接口。
419
+ - 绑定接口由子系统后端提供,例如 `userBind({ hash })`。
420
+ - 绑定成功后要从 URL 中移除 `hash`,避免重复绑定。
421
+ - 绑定失败后要清理 `hash` 或重新进入登录流程,避免死循环。
422
+
423
+ ## 11. extra_param
424
+
425
+ `extraParam` 会被序列化为一个 `extra_param` 字符串,用于把来源系统、绑定信息或其他后端需要的上下文透传给 OIDC 流程:
426
+
427
+ ```ts
428
+ ssoSdk.redirectToLogin({
429
+ returnPath,
430
+ extraParam: {
431
+ source: 'invite',
432
+ },
433
+ });
434
+ ```
435
+
436
+ 生成结构类似:
437
+
438
+ ```text
439
+ /api/oidc/login?return_path=...&extra_param=from=aimo&oidc_server_address=...&source=invite
440
+ ```
441
+
442
+ 接入方通常只需要关心 `extraParam`。绑定流程历史上还需要顶层 query `bind_hash`,这个参数由 SDK 在 `redirectToBind()` 内部自动生成,不需要业务方手动传。
443
+
444
+ ## 12. 401 和 Session 过期处理
445
+
446
+ SDK 不封装业务请求客户端,但提供公共常量和判断函数。
447
+
448
+ ```ts
449
+ import { isUnauthorizedBusinessCode, isUnauthorizedStatus } from 'aid-sso-sdk';
450
+
451
+ if (isUnauthorizedStatus(response.status) || isUnauthorizedBusinessCode(data.code)) {
452
+ ssoSdk.handleUnauthorized({
453
+ returnPath: window.location.href,
454
+ });
455
+ }
456
+ ```
457
+
458
+ SDK 当前默认识别:
459
+
460
+ ```ts
461
+ const HTTP_UNAUTHORIZED_STATUS = 401;
462
+ const BUSINESS_UNAUTHORIZED_CODE = 401;
463
+ const BUSINESS_SESSION_EXPIRED_CODE = 4011;
464
+ const DEFAULT_UNAUTHORIZED_BUSINESS_CODES = [401, 4011] as const;
465
+ ```
466
+
467
+ `handleUnauthorized()` 会执行:
468
+
469
+ ```text
470
+ clearLocalAuth()
471
+ clearSession()
472
+ redirectToLogin()
473
+ ```
474
+
475
+ 如果只想清理,不想立即跳转:
476
+
477
+ ```ts
478
+ ssoSdk.handleUnauthorized({
479
+ redirect: false,
480
+ });
481
+ ```
482
+
483
+ ## 13. 登出
484
+
485
+ 推荐用户点击登出时调用:
486
+
487
+ ```ts
488
+ try {
489
+ await ssoSdk.logout();
490
+ } catch (error) {
491
+ console.error(error);
492
+ }
493
+
494
+ ssoSdk.redirectToLogin();
495
+ ```
496
+
497
+ 也可以让 SDK 在登出成功后直接跳登录:
498
+
499
+ ```ts
500
+ await ssoSdk.logout({
501
+ redirectToLogin: true,
502
+ returnPath: window.location.href,
503
+ });
504
+ ```
505
+
506
+ `logout()` 行为:
507
+
508
+ - 调用 `POST /api/oidc/logout`
509
+ - 执行 `clearLocalAuth()`
510
+ - 清理当前 session cookie
511
+ - 如果后端登出失败,清理仍会执行,然后先抛出错误,不会继续自动跳转登录
512
+ - `redirectToLogin: true` 只在登出接口成功时生效
513
+
514
+ ## 14. AidLux 个人中心
515
+
516
+ 用户菜单中跳转个人中心:
517
+
518
+ ```ts
519
+ const profileUrl = await ssoSdk.getAidProfileUrl({
520
+ targetUserId: user.aid_user_id,
521
+ targetUsername: user.aid_username,
522
+ type: 'account',
523
+ });
524
+
525
+ window.open(profileUrl, '_blank');
526
+ ```
527
+
528
+ SDK 会调用 `GET /api/oidc/aidauth` 获取登录中心地址,并生成 AidLux `/user/profile` 个人中心地址。
529
+
530
+ `type` 只能使用 SSO 当前已有页面:
531
+
532
+ ```ts
533
+ type AidProfilePageType = 'profile' | 'account' | 'password' | 'bind_phone' | 'bind_email';
534
+ ```
535
+
536
+ `/api/oidc/aidauth` 固定返回:
537
+
538
+ ```json
539
+ { "login_page_url": "https://auth.aidlux.com" }
540
+ ```
541
+
542
+ ## 15. 开发代理和部署注意事项
543
+
544
+ 开发环境代理必须确保 `/api/oidc/*` 能到达子系统后端,并且必须设置 `changeOrigin: false`。
545
+
546
+ `changeOrigin: false` 很重要:它可以保留浏览器访问前端时的 Host/来源上下文,避免代理把请求来源改成后端地址,导致后端解析不到真实使用者 IP 或写入错误域名下的 cookie。
547
+
548
+ 如果业务 API 前缀就是 `/api`,通常一条 `/api` 代理即可覆盖:
549
+
550
+ ```ts
551
+ proxy: {
552
+ '/api': {
553
+ target: 'http://your-backend',
554
+ changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
555
+ },
556
+ }
557
+ ```
558
+
559
+ 如果业务 API 前缀不是 `/api`,例如 `/api/v1`,需要额外配置 `/api/oidc`:
560
+
561
+ ```ts
562
+ proxy: {
563
+ '/api/oidc': {
564
+ target: 'http://your-backend',
565
+ changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
566
+ },
567
+ '/api/v1': {
568
+ target: 'http://your-backend',
569
+ changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
570
+ },
571
+ }
572
+ ```
573
+
574
+ 注意:
575
+
576
+ - cookie 方案下,开发代理必须设置 `changeOrigin: false`,避免后端解析不到真实使用者 IP,也避免 cookie 域和前端访问域不一致。
577
+ - 生产 Nginx 也要确保 `/api/oidc/*` 代理到子系统后端。
578
+ - OIDC callback 地址应使用前端访问域下的 `/api/oidc/callback`,让 cookie 写在前端访问域。
579
+
580
+ ## 16. 框架接入示例
581
+
582
+ ### Vue 3 + Vue Router
583
+
584
+ ```ts
585
+ // src/utils/sso.ts
586
+ export const ssoSdk = createSsoSdk({
587
+ cookieName: 'your_session',
588
+ from: 'aimo',
589
+ clearLocalAuth() {
590
+ localStorage.removeItem('user');
591
+ },
592
+ });
593
+ ```
594
+
595
+ ```ts
596
+ // router.beforeEach
597
+ if (!ssoSdk.hasSession()) {
598
+ next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
599
+ return;
600
+ }
601
+ ```
602
+
603
+ ```ts
604
+ // Login.vue
605
+ const returnPath = `${window.location.origin}/#${redirect || '/'}`;
606
+ ssoSdk.redirectToLogin({ returnPath });
607
+ ```
608
+
609
+ ### React / Umi
610
+
611
+ ```ts
612
+ // src/services/oidc.ts
613
+ export const ssoSdk = createSsoSdk({
614
+ cookieName: 'ls_session',
615
+ clearLocalAuth() {
616
+ persist.clear();
617
+ },
618
+ });
619
+ ```
620
+
621
+ ```ts
622
+ // app.tsx onPageChange
623
+ if (!currentUser?.username && !ssoSdk.hasSession()) {
624
+ ssoSdk.redirectToLogin({
625
+ returnPath: window.location.href,
626
+ });
627
+ }
628
+ ```
629
+
630
+ ```ts
631
+ // request interceptor
632
+ if (isUnauthorizedBusinessCode(data.code) || isUnauthorizedStatus(response.status)) {
633
+ ssoSdk.handleUnauthorized({
634
+ returnPath: window.location.href,
635
+ });
636
+ }
637
+ ```
638
+
639
+ ## 17. 验收清单
640
+
641
+ 接入完成后至少验证这些场景:
642
+
643
+ - 未登录访问受保护页面,会进入 SSO 或本地登录页。
644
+ - 登录成功后能回到原页面。
645
+ - hash 路由系统登录后能回到正确 hash 页面。
646
+ - history 路由系统刷新深层路径不丢失。
647
+ - 业务接口能自动携带 session cookie。
648
+ - session 过期时,业务接口 401 能清理本地状态并重新登录。
649
+ - 点击登出后,服务端 session 和前端 cookie 都被清理。
650
+ - 登出后访问受保护页面会重新进入登录流程。
651
+ - 个人中心能打开正确 AidLux `/user/profile` 页面。
652
+ - 绑定流程能携带 `hash`、`username`、`bind_hash` 并完成回跳。
653
+ - 本地登录页模式下,`returnPath` 不会错误指回 `/login`。
654
+ - `/api/oidc/*` 没有经过业务 request 的 `{ code: 200 }` 拦截器。
655
+
656
+ ## 18. 常见问题
657
+
658
+ ### 登录后还是判断未登录
659
+
660
+ 检查:
661
+
662
+ - 后端是否写入了正确 cookie 名称。
663
+ - cookie 是否设置了 `HttpOnly`。
664
+ - cookie domain/path 是否能被当前前端页面读取。
665
+ - 业务请求是否设置 `withCredentials: true` 或 `credentials: 'include'`。
666
+
667
+ ### 登录后回到了 `/login`
668
+
669
+ 通常是本地登录页传错了 `returnPath`。
670
+
671
+ 有本地登录页时,不要直接使用当前登录页的 `window.location.href`,应读取 `redirect` 后计算最终业务地址。
672
+
673
+ ### `/api/oidc/logout` 或 `/api/oidc/aidauth` 被业务拦截器报错
674
+
675
+ 说明 OIDC 接口可能走了子系统业务 request 封装。
676
+
677
+ SDK 内部会用原生 `fetch` 调用 OIDC 原始接口,业务代码不要再用 axios/umi-request 包一层调用这些接口。
678
+
679
+ ### 新子系统的 `from` 类型不允许
680
+
681
+ 当前 SDK 已按 SSO 现有值固定为:
682
+
683
+ ```ts
684
+ type SsoSubsystemFrom = 'aimo' | 'aic';
685
+ ```
686
+
687
+ 新增来源前,需要先确认 SSO 侧是否支持,再扩展 SDK 类型。
688
+
689
+ ## 19. API 摘要
690
+
691
+ ```ts
692
+ type OidcBasePath = '/api/oidc';
693
+ type SsoSubsystemFrom = 'aimo' | 'aic';
694
+ type AidProfilePageType = 'profile' | 'account' | 'password' | 'bind_phone' | 'bind_email';
695
+
696
+ type SsoSdkOptions = {
697
+ cookieName: string;
698
+ oidcBasePath?: OidcBasePath;
699
+ from?: SsoSubsystemFrom;
700
+ oidcServerAddress?: string | (() => string);
701
+ navigate?: (url: string) => void;
702
+ clearLocalAuth?: () => void;
703
+ };
704
+
705
+ type SsoSdk = {
706
+ hasSession(): boolean;
707
+ clearSession(): void;
708
+ getLoginUrl(input?: { returnPath?: string; extraParam?: ExtraParamMap }): string;
709
+ redirectToLogin(input?: { returnPath?: string; extraParam?: ExtraParamMap }): void;
710
+ getRegisterUrl(input?: { returnPath?: string; extraParam?: ExtraParamMap }): string;
711
+ redirectToRegister(input?: { returnPath?: string; extraParam?: ExtraParamMap }): void;
712
+ redirectToBind(input: BindInput): void;
713
+ logout(input?: { redirectToLogin?: boolean; returnPath?: string }): Promise<void>;
714
+ getAidProfileUrl(input?: AidProfileUrlInput): Promise<string>;
715
+ handleUnauthorized(input?: { redirect?: boolean; returnPath?: string }): void;
716
+ };
717
+
718
+ const HTTP_UNAUTHORIZED_STATUS = 401;
719
+ const BUSINESS_UNAUTHORIZED_CODE = 401;
720
+ const BUSINESS_SESSION_EXPIRED_CODE = 4011;
721
+ const DEFAULT_UNAUTHORIZED_BUSINESS_CODES = [401, 4011] as const;
722
+
723
+ function isUnauthorizedStatus(status?: number | null): boolean;
724
+ function isUnauthorizedBusinessCode(code?: number | string | null): boolean;
725
+ ```