aid-npm-test987-sso-sdk
v0.1.2
Published
Framework-agnostic browser SDK for AidLux OIDC SSO integration.
Maintainers
Readme
aid-sso-sdk
AidLux OIDC SSO 前端接入 SDK,面向浏览器环境,和具体前端技术栈无关。
SDK 只封装各子系统接入 SSO 时稳定、公共的前端逻辑:
- 判断 session cookie 是否存在
- 构造
/api/oidc/*登录、注册、绑定跳转地址 - 使用原生
fetch调用 OIDC 原始接口 - 登出、401 清理、AidLux 个人中心地址构造
- 账号绑定流程参数拼装
- 未授权状态码和业务 code 的公共判断
SDK 不依赖 Vue、React、Next.js、Umi、Pinia、Redux、Axios,也不使用任何子系统自己的 request 封装。
适用范围
适合这些子系统:
- 使用 cookie session 接入 AidLux OIDC SSO
- 后端已提供统一
/api/oidc/*接口 - 前端需要兼容 hash 路由、history 路由、有本地登录页或无本地登录页
- 需要统一处理登出、401、个人中心、旧账号绑定
SDK 不负责这些业务逻辑:
- 子系统自己的用户信息接口,例如
/api/user/info - 菜单权限、角色权限、账号激活状态
- Pinia、Redux、Umi initialState 等状态管理细节
- 本地登录页的 UI 和路由跳转策略
- 离线私有化 token 模式
- 子系统业务 request 的完整封装
接入总览
一个新子系统建议按下面顺序接入:
- 后端提供固定
/api/oidc/*接口。 - 后端设置前端可读的 session cookie。
- 前端安装
aid-sso-sdk。 - 创建子系统自己的
ssoSdk单例。 - 配置业务请求携带 cookie。
- 在路由守卫或应用初始化里判断登录态。
- 根据产品形态选择“直接跳 SSO”或“先进入本地登录页”。
- 登录页或登录按钮调用 SDK 跳转 SSO。
- 注册入口调用 SDK 跳转 SSO 注册。
- 旧账号迁移场景调用 SDK 绑定流程。
- 业务请求拦截器使用 SDK 判断 401 并清理状态。
- 用户菜单接入登出和 AidLux 个人中心。
- 按验收清单验证完整流程。
1. 后端接口准备
所有子系统后端必须提供固定 OIDC 接口:
GET /api/oidc/login
GET /api/oidc/register
GET /api/oidc/callback
POST /api/oidc/logout
GET /api/oidc/aidauth接口职责:
/api/oidc/login:接收return_path和可选extra_param,然后 302 到 SSO 登录。/api/oidc/register:接收return_path和可选extra_param,然后进入 SSO 注册流程。/api/oidc/callback:由 OIDC Provider 回调,后端完成 code 换 token、创建 session、写 cookie,然后 302 回return_path。/api/oidc/logout:清理服务端 session,并通过Set-Cookie: Max-Age=0清理 cookie。/api/oidc/aidauth:返回 AidLux 登录中心地址,用于前端构造个人中心 URL。
前端 SDK 不处理 OIDC 的 client id、client secret、authorize、token 交换等后端细节。
2. Cookie 约定
每个子系统都需要确定自己的 session cookie 名称:
const COOKIE_NAME = 'your_system_session';要求:
- cookie 由子系统后端设置。
- cookie 名称不要和其他子系统冲突。
- cookie 必须允许前端读取,不能设置
HttpOnly。 - 前端只判断 cookie 是否存在,不解析 cookie 值。
- 业务请求通过浏览器自动携带 cookie 完成鉴权。
已有示例:
aimo -> aimo_session
license-manger -> ls_session3. 安装 SDK
开发阶段通过本地目录依赖安装:
pnpm add file:../aid-sso-sdk发布到 npm 源后:
npm install aid-sso-sdk如果项目使用 pnpm,建议保持锁文件只使用 pnpm-lock.yaml,不要同时维护多种 lockfile。
4. 创建 SSO SDK
建议在子系统里新建一个独立文件:
src/utils/sso.ts示例:
import { createSsoSdk } from 'aid-sso-sdk';
export const ssoSdk = createSsoSdk({
cookieName: 'your_system_session',
from: 'aimo',
clearLocalAuth() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
},
});配置说明:
type SsoSdkOptions = {
cookieName: string;
oidcBasePath?: '/api/oidc';
from?: 'aimo' | 'aic';
oidcServerAddress?: string | (() => string);
navigate?: (url: string) => void;
clearLocalAuth?: () => void;
};字段说明:
cookieName:必填,必须和后端写入的 session cookie 名一致。oidcBasePath:默认/api/oidc,当前规范固定为该值。from:来源子系统标识。当前 SDK 已固定为'aimo' | 'aic',新增来源前需要先确认 SSO 是否支持。oidcServerAddress:子系统前端访问地址,默认window.location.origin。navigate:自定义跳转函数,默认window.location.assign(url)。clearLocalAuth:清理子系统自己的本地登录状态,SDK 会在登出和 401 处理中调用。
from 和 oidcServerAddress 是配套参数。只要配置了 from,SDK 就会在 extra_param 中配套传递 oidc_server_address;如果调用方没有显式传 oidcServerAddress,SDK 默认使用 window.location.origin。
这两个参数主要用于注册流程:SSO 可以根据来源子系统和子系统访问地址,拉取或填写对应子系统的申请试用表单信息。
5. 业务代码直接使用 ssoSdk
SDK 创建的 ssoSdk 实例本身就是一个语义清晰的封装。业务代码直接导入使用,不需要再给每个方法包一层空函数:
import { ssoSdk } from '@/utils/sso';
// 判断登录态
if (!ssoSdk.hasSession()) {
ssoSdk.redirectToLogin();
}
// 登出
await ssoSdk.logout();ssoSdk.hasSession() 一眼就能看出它来自 SDK;包一层 hasOidcSession() 反而多了一层间接,需要跳转到包装文件才能定位实现。
什么时候才值得包装
只有一种情况:你需要改变 SDK 的默认行为——比如追加子系统特有的默认参数,或者在 SDK 方法前后执行额外的逻辑。
// 场景 1:带上项目特有的 extraParam
export const redirectToLogin = (returnPath?: string) =>
ssoSdk.redirectToLogin({
returnPath,
extraParam: { source: 'license_page' },
});
// 场景 2:退出时额外清理子系统缓存
export const logout = async () => {
clearLocalCache();
await ssoSdk.logout();
};空包装——函数体只有一个 SDK 方法调用、不加任何额外逻辑——不需要写。让业务代码直接用 ssoSdk 即可。
6. 配置业务请求携带 Cookie
在线 OIDC 模式下,业务请求必须允许携带 cookie。
Axios 示例:
axios.create({
baseURL: '/api',
withCredentials: true,
});Umi request 示例:
const request = extend({
prefix: '/api',
credentials: 'include',
});注意事项:
- 在线 OIDC 模式下,不要再给业务请求手动注入
Authorization。 - 鉴权凭证由浏览器自动携带 session cookie。
- 如果项目同时存在离线 token 模式,离线逻辑保留在子系统内部,不放进 SDK。
7. 选择登录入口模式
子系统通常有两种登录入口形态。
7.1 无本地登录页:直接跳 SSO
适合没有自己登录页的系统,例如 license-manger。
路由守卫、页面切换或应用初始化时:
if (!ssoSdk.hasSession()) {
ssoSdk.redirectToLogin({
returnPath: window.location.href,
});
}如果不传 returnPath,SDK 默认使用 window.location.href:
ssoSdk.redirectToLogin();7.2 有本地登录页:先进入本地 /login
适合需要保留本地登录壳、账号密码登录、注册入口、绑定提示的系统,例如 aimo。
路由守卫仍由子系统自己控制:
router.push(`/login?redirect=${encodeURIComponent(to.fullPath)}`);登录页读取 redirect,计算最终 SSO 回跳地址:
const redirect = route.query.redirect as string | undefined;
const returnPath = `${window.location.origin}/#${redirect || '/'}`;
ssoSdk.redirectToLogin({ returnPath });注意:
- SDK 不决定是否先进入子系统
/login。 - SDK 只负责最后跳
/api/oidc/login。 - hash 路由、history 路由、本地登录页 redirect 参数由子系统自己计算。
8. return_path 规则
SDK 的 return_path 规则只有两条,由 getLoginUrl()、redirectToLogin()、getRegisterUrl()、redirectToRegister() 等方法内部处理:
- 传了
returnPath:原样使用。 - 没传:使用
window.location.href。
常见写法:
history 路由当前页直接跳:
ssoSdk.redirectToLogin();hash 路由在本地登录页里跳:
const returnPath = `${window.location.origin}/#${redirect || '/'}`;
ssoSdk.redirectToLogin({ returnPath });不要让 SDK 猜测路由模式。子系统最清楚自己的路由结构,应主动传入准确的 returnPath。
9. 登录和注册
构造登录地址但不跳转:
const loginUrl = ssoSdk.getLoginUrl({ returnPath });直接跳转登录:
ssoSdk.redirectToLogin({ returnPath });构造注册地址但不跳转:
const registerUrl = ssoSdk.getRegisterUrl({ returnPath });直接跳转注册:
ssoSdk.redirectToRegister({ returnPath });登录和注册使用相同的 return_path 和 extra_param 规则。
10. 旧账号绑定流程
绑定流程分为两段:
- 跳转 SSO 前:子系统调用
redirectToBind(),把绑定所需参数带到 SSO。 - SSO 登录并回到子系统后:子系统在路由守卫中判断 URL query 里的
hash,调用自己的绑定接口完成绑定。
SDK 只负责第一段“跳转并携带参数”。第二段“回来后调用子系统绑定接口”必须由子系统实现,因为绑定接口属于各子系统后端。
当子系统旧账号登录后,后端返回需要绑定 AidLux 账号时,跳转前调用:
ssoSdk.redirectToBind({
hash,
username,
returnPath,
});SDK 会自动处理:
extra_param.hashextra_param.usernameextra_param.bind_hashextra_param.fromextra_param.oidc_server_address- 顶层 query
bind_hash
如果后端返回了独立的 bindHash:
ssoSdk.redirectToBind({
hash,
bindHash,
username,
returnPath,
});如果还有额外绑定参数:
ssoSdk.redirectToBind({
hash,
username,
returnPath,
extraParam: {
source: 'migration',
},
});redirectToBind() 最终会跳转到:
/api/oidc/login?return_path=...&bind_hash=...&extra_param=...SSO 登录完成后,子系统后端 callback 会把 extra_param 里的 hash 带回子系统前端路由。子系统需要在路由守卫中处理:
router.beforeEach(async (to, _, next) => {
const query = to.query;
if (query?.hash) {
try {
await userBind({ hash: query.hash as string });
// 绑定成功后移除 hash,避免刷新或再次进入路由时重复绑定
to.query = omit(to.query, ['hash']);
next(to);
return;
} catch (error) {
// 绑定失败时应清理 hash,避免登录/绑定循环
to.query = omit(to.query, ['hash', 'redirect']);
next('/login');
return;
}
}
next();
});aimo 当前就是这种处理方式:在 src/router/index.ts 中判断 query.hash,然后调用 userBind({ hash })。离线 token 回调场景下,也是在处理 access_token 后继续判断 query.hash 并调用同一个绑定接口。
注意事项:
redirectToBind()不会调用子系统绑定接口。- 绑定接口由子系统后端提供,例如
userBind({ hash })。 - 绑定成功后要从 URL 中移除
hash,避免重复绑定。 - 绑定失败后要清理
hash或重新进入登录流程,避免死循环。
11. extra_param
extraParam 会被序列化为一个 extra_param 字符串,用于把来源系统、绑定信息或其他后端需要的上下文透传给 OIDC 流程:
ssoSdk.redirectToLogin({
returnPath,
extraParam: {
source: 'invite',
},
});生成结构类似:
/api/oidc/login?return_path=...&extra_param=from=aimo&oidc_server_address=...&source=invite接入方通常只需要关心 extraParam。绑定流程历史上还需要顶层 query bind_hash,这个参数由 SDK 在 redirectToBind() 内部自动生成,不需要业务方手动传。
12. 401 和 Session 过期处理
SDK 不封装业务请求客户端,但提供公共常量和判断函数。
import { isUnauthorizedBusinessCode, isUnauthorizedStatus } from 'aid-sso-sdk';
if (isUnauthorizedStatus(response.status) || isUnauthorizedBusinessCode(data.code)) {
ssoSdk.handleUnauthorized({
returnPath: window.location.href,
});
}SDK 当前默认识别:
const HTTP_UNAUTHORIZED_STATUS = 401;
const BUSINESS_UNAUTHORIZED_CODE = 401;
const BUSINESS_SESSION_EXPIRED_CODE = 4011;
const DEFAULT_UNAUTHORIZED_BUSINESS_CODES = [401, 4011] as const;handleUnauthorized() 会执行:
clearLocalAuth()
clearSession()
redirectToLogin()如果只想清理,不想立即跳转:
ssoSdk.handleUnauthorized({
redirect: false,
});13. 登出
推荐用户点击登出时调用:
try {
await ssoSdk.logout();
} catch (error) {
console.error(error);
}
ssoSdk.redirectToLogin();也可以让 SDK 在登出成功后直接跳登录:
await ssoSdk.logout({
redirectToLogin: true,
returnPath: window.location.href,
});logout() 行为:
- 调用
POST /api/oidc/logout - 执行
clearLocalAuth() - 清理当前 session cookie
- 如果后端登出失败,清理仍会执行,然后先抛出错误,不会继续自动跳转登录
redirectToLogin: true只在登出接口成功时生效
14. AidLux 个人中心
用户菜单中跳转个人中心:
const profileUrl = await ssoSdk.getAidProfileUrl({
targetUserId: user.aid_user_id,
targetUsername: user.aid_username,
type: 'account',
});
window.open(profileUrl, '_blank');SDK 会调用 GET /api/oidc/aidauth 获取登录中心地址,并生成 AidLux /user/profile 个人中心地址。
type 只能使用 SSO 当前已有页面:
type AidProfilePageType = 'profile' | 'account' | 'password' | 'bind_phone' | 'bind_email';/api/oidc/aidauth 固定返回:
{ "login_page_url": "https://auth.aidlux.com" }15. 开发代理和部署注意事项
开发环境代理必须确保 /api/oidc/* 能到达子系统后端,并且必须设置 changeOrigin: false。
changeOrigin: false 很重要:它可以保留浏览器访问前端时的 Host/来源上下文,避免代理把请求来源改成后端地址,导致后端解析不到真实使用者 IP 或写入错误域名下的 cookie。
如果业务 API 前缀就是 /api,通常一条 /api 代理即可覆盖:
proxy: {
'/api': {
target: 'http://your-backend',
changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
},
}如果业务 API 前缀不是 /api,例如 /api/v1,需要额外配置 /api/oidc:
proxy: {
'/api/oidc': {
target: 'http://your-backend',
changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
},
'/api/v1': {
target: 'http://your-backend',
changeOrigin: false, // 必须设置,避免后端解析不到真实使用者 IP
},
}注意:
- cookie 方案下,开发代理必须设置
changeOrigin: false,避免后端解析不到真实使用者 IP,也避免 cookie 域和前端访问域不一致。 - 生产 Nginx 也要确保
/api/oidc/*代理到子系统后端。 - OIDC callback 地址应使用前端访问域下的
/api/oidc/callback,让 cookie 写在前端访问域。
16. 框架接入示例
Vue 3 + Vue Router
// src/utils/sso.ts
export const ssoSdk = createSsoSdk({
cookieName: 'your_session',
from: 'aimo',
clearLocalAuth() {
localStorage.removeItem('user');
},
});// router.beforeEach
if (!ssoSdk.hasSession()) {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
return;
}// Login.vue
const returnPath = `${window.location.origin}/#${redirect || '/'}`;
ssoSdk.redirectToLogin({ returnPath });React / Umi
// src/services/oidc.ts
export const ssoSdk = createSsoSdk({
cookieName: 'ls_session',
clearLocalAuth() {
persist.clear();
},
});// app.tsx onPageChange
if (!currentUser?.username && !ssoSdk.hasSession()) {
ssoSdk.redirectToLogin({
returnPath: window.location.href,
});
}// request interceptor
if (isUnauthorizedBusinessCode(data.code) || isUnauthorizedStatus(response.status)) {
ssoSdk.handleUnauthorized({
returnPath: window.location.href,
});
}17. 验收清单
接入完成后至少验证这些场景:
- 未登录访问受保护页面,会进入 SSO 或本地登录页。
- 登录成功后能回到原页面。
- hash 路由系统登录后能回到正确 hash 页面。
- history 路由系统刷新深层路径不丢失。
- 业务接口能自动携带 session cookie。
- session 过期时,业务接口 401 能清理本地状态并重新登录。
- 点击登出后,服务端 session 和前端 cookie 都被清理。
- 登出后访问受保护页面会重新进入登录流程。
- 个人中心能打开正确 AidLux
/user/profile页面。 - 绑定流程能携带
hash、username、bind_hash并完成回跳。 - 本地登录页模式下,
returnPath不会错误指回/login。 /api/oidc/*没有经过业务 request 的{ code: 200 }拦截器。
18. 常见问题
登录后还是判断未登录
检查:
- 后端是否写入了正确 cookie 名称。
- cookie 是否设置了
HttpOnly。 - cookie domain/path 是否能被当前前端页面读取。
- 业务请求是否设置
withCredentials: true或credentials: 'include'。
登录后回到了 /login
通常是本地登录页传错了 returnPath。
有本地登录页时,不要直接使用当前登录页的 window.location.href,应读取 redirect 后计算最终业务地址。
/api/oidc/logout 或 /api/oidc/aidauth 被业务拦截器报错
说明 OIDC 接口可能走了子系统业务 request 封装。
SDK 内部会用原生 fetch 调用 OIDC 原始接口,业务代码不要再用 axios/umi-request 包一层调用这些接口。
新子系统的 from 类型不允许
当前 SDK 已按 SSO 现有值固定为:
type SsoSubsystemFrom = 'aimo' | 'aic';新增来源前,需要先确认 SSO 侧是否支持,再扩展 SDK 类型。
19. API 摘要
type OidcBasePath = '/api/oidc';
type SsoSubsystemFrom = 'aimo' | 'aic';
type AidProfilePageType = 'profile' | 'account' | 'password' | 'bind_phone' | 'bind_email';
type SsoSdkOptions = {
cookieName: string;
oidcBasePath?: OidcBasePath;
from?: SsoSubsystemFrom;
oidcServerAddress?: string | (() => string);
navigate?: (url: string) => void;
clearLocalAuth?: () => void;
};
type SsoSdk = {
hasSession(): boolean;
clearSession(): void;
getLoginUrl(input?: { returnPath?: string; extraParam?: ExtraParamMap }): string;
redirectToLogin(input?: { returnPath?: string; extraParam?: ExtraParamMap }): void;
getRegisterUrl(input?: { returnPath?: string; extraParam?: ExtraParamMap }): string;
redirectToRegister(input?: { returnPath?: string; extraParam?: ExtraParamMap }): void;
redirectToBind(input: BindInput): void;
logout(input?: { redirectToLogin?: boolean; returnPath?: string }): Promise<void>;
getAidProfileUrl(input?: AidProfileUrlInput): Promise<string>;
handleUnauthorized(input?: { redirect?: boolean; returnPath?: string }): void;
};
const HTTP_UNAUTHORIZED_STATUS = 401;
const BUSINESS_UNAUTHORIZED_CODE = 401;
const BUSINESS_SESSION_EXPIRED_CODE = 4011;
const DEFAULT_UNAUTHORIZED_BUSINESS_CODES = [401, 4011] as const;
function isUnauthorizedStatus(status?: number | null): boolean;
function isUnauthorizedBusinessCode(code?: number | string | null): boolean;