This commit is contained in:
Eric
2026-02-11 11:04:23 +08:00
parent cdf731a052
commit 0495cb488e
812 changed files with 5138 additions and 1979 deletions

View File

@@ -0,0 +1,60 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# IDE-specific files
.idea/
*.iml
*.iws
*.ipr
.vscode/
.project
.classpath
.settings/
# OS generated files
.DS_Store
Thumbs.db
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
# SonarQube
.sonar/
# Test coverage
jacoco.exec

View File

@@ -0,0 +1,119 @@
# Unified Login SDK
统一登录前端SDK基于OAuth2协议实现前后端分离项目的认证和权限管理。
## 安装
```bash
npm install unified-login-sdk
```
## 快速开始
### 基础使用
```javascript
import unifiedLoginSDK from 'unified-login-sdk';
// 初始化配置
unifiedLoginSDK.init({
clientId: 'your-client-id',
basepath: 'https://api.example.com',
homePage: '/dashboard',
idpLogoutUrl: 'https://idp.example.com/logout'
});
// 检查登录状态
if (!unifiedLoginSDK.isAuthenticated()) {
// 执行登录
await unifiedLoginSDK.login();
}
// 获取用户信息
const userInfo = unifiedLoginSDK.getUserInfo();
console.log('欢迎:', userInfo.nickName);
```
### Vue 3 集成
```javascript
// main.js
import { createApp } from 'vue';
import { createVuePlugin } from 'unified-login-sdk';
const app = createApp(App);
const loginPlugin = createVuePlugin('localStorage');
app.use(loginPlugin, {
config: {
clientId: 'your-client-id',
basepath: 'https://api.example.com',
homePage: '/dashboard',
idpLogoutUrl: 'https://idp.example.com/logout'
}
});
app.mount('#app');
```
### 权限检查
```javascript
// 检查单个权限
const hasPermission = await unifiedLoginSDK.hasPermission('user:read');
// 检查多个权限
const hasAllPermissions = await unifiedLoginSDK.hasAllPermissions(['user:read', 'user:write']);
// 检查角色
const hasRole = await unifiedLoginSDK.hasRole('admin');
```
## 核心功能
- ✅ OAuth2认证流程
- ✅ Token自动管理
- ✅ 用户信息获取
- ✅ 权限和角色检查
- ✅ Vue 2/3插件支持
- ✅ 路由守卫集成
- ✅ 事件监听机制
## API参考
### 主要方法
| 方法 | 说明 | 参数 | 返回值 |
|------|------|------|--------|
| `init(config)` | 初始化SDK | 配置对象 | void |
| `login()` | 执行登录 | redirectUri(可选) | Promise<void> |
| `logout()` | 退出登录 | 无 | Promise<void> |
| `isAuthenticated()` | 检查认证状态 | 无 | boolean |
| `getUserInfo()` | 获取用户信息 | 无 | UserInfo |
| `hasPermission(permission)` | 检查权限 | 权限标识 | Promise<boolean> |
| `hasRole(role)` | 检查角色 | 角色标识 | Promise<boolean> |
### 事件监听
```javascript
// 登录事件
unifiedLoginSDK.on('login', () => {
console.log('用户已登录');
});
// 退出事件
unifiedLoginSDK.on('logout', () => {
console.log('用户已退出');
});
```
## 浏览器支持
- Chrome (推荐)
- Firefox
- Safari
- Edge
## 许可证
MIT

View File

@@ -0,0 +1,101 @@
/**
* 认证核心逻辑
* 实现OAuth2授权码模式的完整流程
*/
import { EventType, RouterInfo, SDKConfig, UserInfo } from '../types';
import { Storage } from '../utils/storage';
/**
* 认证核心类
*/
export declare class Auth {
private config;
private tokenManager;
private httpClient;
private storage;
private eventHandlers;
private userInfoCache;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage);
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: SDKConfig): void;
getToken(): string | null;
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
login(redirectUri?: string): Promise<void>;
/**
* 退出登录
*/
logout(): Promise<void>;
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
handleCallback(): Promise<void>;
getRoutes(): Promise<RouterInfo>;
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
getUserInfo(): UserInfo;
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
hasRole(role: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
hasAllRoles(roles: string[]): Promise<boolean>;
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
hasPermission(permission: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
hasAllPermissions(permissions: string[]): Promise<boolean>;
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean;
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: EventType, callback: Function): void;
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: EventType, callback: Function): void;
/**
* 触发事件
* @param event 事件类型
* @param data 事件数据
*/
private emit;
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean;
}
//# sourceMappingURL=auth.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAC;AAGpE,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAC;AAGzC;;GAEG;AACH,qBAAa,IAAI;IACf,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,aAAa,CAInB;IACF,OAAO,CAAC,aAAa,CAAyB;IAE9C;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAQ5B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAM7B,QAAQ,IAAG,MAAM,GAAG,IAAI;IAIxB;;;OAGG;IACG,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC/B,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAYtC;;;OAGG;IACF,WAAW,IAAI,QAAQ;IAOxB;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBxD;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAWpD;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBpE;;;;OAIG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhE;;;OAGG;IACH,eAAe,IAAI,OAAO;IAK1B;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI9C;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI/C;;;;OAIG;IACH,OAAO,CAAC,IAAI;IAUZ;;;OAGG;IACH,UAAU,IAAI,OAAO;CAGtB"}

View File

@@ -0,0 +1,241 @@
/**
* 认证核心逻辑
* 实现OAuth2授权码模式的完整流程
*/
import { TokenManager } from './token';
import { HttpClient } from './http';
import { buildQueryParams, isCallbackUrl, parseQueryParams } from '../utils/url';
/**
* 认证核心类
*/
export class Auth {
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage) {
this.config = null;
this.eventHandlers = {
login: [],
logout: [],
tokenExpired: []
};
this.userInfoCache = null;
this.storage = storage;
// 先创建HttpClient初始时tokenManager为undefined
this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);
// 然后创建TokenManager
this.tokenManager = new TokenManager(storage);
}
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config) {
this.config = config;
// 设置租户ID到HTTP客户端
this.httpClient.setTenantId(config.tenantId);
}
getToken() {
return this.tokenManager.getToken();
}
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
async login(redirectUri) {
if (!this.config) {
throw new Error('SDK not initialized');
}
const registrationId = this.config.registrationId || 'idp';
const basepath = this.config.basepath || '';
const path = `${basepath}/oauth2/authorization/${registrationId}`;
const tokenResponse = await this.httpClient.get(path, { needAuth: false });
const redirect = tokenResponse.data.redirect_url;
const params = parseQueryParams(redirect);
this.storage.set(params.state, window.location.href);
window.location.href = redirect;
}
/**
* 退出登录
*/
async logout() {
if (!this.config) {
throw new Error('SDK not initialized');
}
// 清除本地存储的Token和用户信息
this.tokenManager.clearToken();
this.userInfoCache = null;
this.storage.remove('userInfo');
const basepath = this.config.basepath || '';
await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true });
// 触发退出事件
this.emit('logout');
window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage;
}
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
async handleCallback() {
if (!this.config) {
throw new Error('SDK not initialized');
}
const params = parseQueryParams();
// 检查是否有错误
if (params.error) {
throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);
}
// 检查是否有授权码
if (!params.code) {
throw new Error('Authorization code not found');
}
const registrationId = this.config.registrationId || 'idp';
const basepath = this.config.basepath || '';
const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`;
const tokenResponse = await this.httpClient.get(callback, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
needAuth: false
});
// 触发登录事件
this.emit('login');
this.storage.set('userInfo', tokenResponse.data.data);
this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']);
let url = this.config.homePage;
if (params.state) {
url = this.storage.get(params.state) || url;
}
window.location.href = url;
}
async getRoutes() {
if (!this.config) {
throw new Error('SDK not initialized');
}
const basepath = this.config.basepath || '';
const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true });
if (tokenResponse.status === 401) {
await this.logout();
}
return tokenResponse.data.data;
}
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
getUserInfo() {
return this.storage.get("userInfo");
}
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
async hasRole(role) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const roleCodes = userInfo.roles || [];
if (Array.isArray(role)) {
// 检查是否有任一角色
return role.some(r => roleCodes.includes(r));
}
// 检查是否有单个角色
return roleCodes.includes(role);
}
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
async hasAllRoles(roles) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const roleCodes = userInfo.roles || [];
// 检查是否有所有角色
return roles.every(r => roleCodes.includes(r));
}
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
async hasPermission(permission) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const permissions = userInfo.permissions || [];
if (Array.isArray(permission)) {
// 检查是否有任一权限
return permission.some(p => permissions.includes(p));
}
// 检查是否有单个权限
return permissions.includes(permission);
}
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
async hasAllPermissions(permissions) {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = this.storage.get("userInfo");
const userPermissions = userInfo.permissions || [];
// 检查是否有所有权限
return permissions.every(p => userPermissions.includes(p));
}
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated() {
// 检查Token是否存在且未过期
return !!this.tokenManager.getToken();
}
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event, callback) {
this.eventHandlers[event].push(callback);
}
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event, callback) {
this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);
}
/**
* 触发事件
* @param event 事件类型
* @param data 事件数据
*/
emit(event, data) {
this.eventHandlers[event].forEach(handler => {
try {
handler(data);
}
catch (error) {
console.error(`Error in ${event} event handler:`, error);
}
});
}
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback() {
return isCallbackUrl();
}
}
//# sourceMappingURL=auth.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,155 @@
/**
* HTTP客户端
* 用于与后端API进行通信
*/
/**
* HTTP请求方法类型
*/
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/**
* HTTP请求选项
*/
export interface HttpRequestOptions {
/** 请求方法 */
method: HttpMethod;
/** 请求URL */
url: string;
/** 请求头 */
headers?: Record<string, string>;
/** 请求体 */
body?: any;
/** 是否需要认证 */
needAuth?: boolean;
}
/**
* HTTP响应类型
*/
export interface HttpResponse<T = any> {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 响应体 */
data: T;
/** 响应头 */
headers: Record<string, string>;
}
/**
* HTTP错误类型
*/
export declare class HttpError extends Error {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 错误数据 */
data: any;
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
*/
constructor(message: string, status: number, statusText: string, data: any);
}
/**
* HTTP客户端类
*/
export declare class HttpClient {
private tokenGetter?;
private tenantId?;
/**
* 构造函数
* @param logout
* @param tokenGetter Token获取函数
*/
constructor(tokenGetter?: () => string | null);
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter: () => string | null): void;
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId?: string): void;
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
/**
* 处理响应数据
* @param response 响应对象
* @param responseData 响应数据
* @returns HttpResponse<T> 处理后的响应
*/
private handleResponse;
/**
* 检查是否为业务响应结构
* @param responseData 响应数据
* @returns boolean 是否为业务响应结构
*/
private isBusinessResponse;
/**
* 获取错误信息
* @param responseData 响应数据
* @returns string 错误信息
*/
private getErrorMessage;
/**
* GET请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
get<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>>;
/**
* POST请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
post<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* PUT请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
put<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* DELETE请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
delete<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>>;
/**
* PATCH请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
patch<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>>;
/**
* 解析响应体
* @param response 响应对象
* @returns Promise<any> 解析后的响应体
*/
private parseResponse;
/**
* 解析响应头
* @param headers 响应头对象
* @returns Record<string, string> 解析后的响应头
*/
private parseHeaders;
}
export {};
//# sourceMappingURL=http.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,KAAK,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU;IACV,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU;IACV,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,aAAa;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,GAAG;IACnC,UAAU;IACV,MAAM,EAAE,MAAM,CAAC;IACf,WAAW;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU;IACV,IAAI,EAAE,CAAC,CAAC;IACR,UAAU;IACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU;IACH,MAAM,EAAE,MAAM,CAAC;IACtB,WAAW;IACJ,UAAU,EAAE,MAAM,CAAC;IAC1B,WAAW;IACJ,IAAI,EAAE,GAAG,CAAC;IAEjB;;;;;;OAMG;gBACS,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;CAO3E;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,WAAW,CAAC,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAC,CAAS;IAE1B;;;;OAIG;gBACS,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI;IAK7C;;;OAGG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI;IAItD;;;OAGG;IACH,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;OAIG;IACG,OAAO,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IA0F7E;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAmCtB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAQ1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAUvB;;;;;OAKG;IACG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAQ/G;;;;;;OAMG;IACG,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IASrI;;;;;;OAMG;IACG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IASpI;;;;;OAKG;IACG,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAQlH;;;;;;OAMG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAStI;;;;OAIG;YACW,aAAa;IAY3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAOrB"}

View File

@@ -0,0 +1,274 @@
/**
* HTTP客户端
* 用于与后端API进行通信
*/
/**
* HTTP错误类型
*/
export class HttpError extends Error {
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
*/
constructor(message, status, statusText, data) {
super(message);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.data = data;
}
}
/**
* HTTP客户端类
*/
export class HttpClient {
/**
* 构造函数
* @param logout
* @param tokenGetter Token获取函数
*/
constructor(tokenGetter) {
this.tokenGetter = tokenGetter;
}
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter) {
this.tokenGetter = tokenGetter;
}
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId) {
this.tenantId = tenantId;
}
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async request(options) {
const { method, url, headers = {}, body, needAuth = true } = options;
// 构建请求头
const requestHeaders = {
'Content-Type': 'application/json',
...headers
};
// 添加认证头
const addAuthHeader = () => {
if (needAuth && this.tokenGetter) {
const token = this.tokenGetter();
if (token) {
requestHeaders.Authorization = `${token}`;
}
}
};
// 添加租户ID头
if (this.tenantId) {
requestHeaders['tenant-id'] = this.tenantId;
}
addAuthHeader();
// 构建请求配置
const fetchOptions = {
method,
headers: requestHeaders,
credentials: 'include' // 包含cookie
};
// 添加请求体
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
try {
// 发送请求
const response = await fetch(url, fetchOptions);
const responseData = await this.parseResponse(response);
// 检查响应状态
if (!response.ok) {
// 如果是401错误尝试刷新Token并重试
if (response.status === 401) {
return {
status: response.status,
statusText: response.statusText,
data: '',
headers: this.parseHeaders(response.headers)
};
}
// 其他错误,直接抛出
const errorMsg = this.getErrorMessage(responseData);
throw new HttpError(errorMsg, response.status, response.statusText, responseData);
}
// 处理成功响应的业务逻辑
return this.handleResponse(response, responseData);
}
catch (error) {
if (error instanceof HttpError) {
throw error;
}
// 网络错误或其他错误
throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null);
}
}
/**
* 处理响应数据
* @param response 响应对象
* @param responseData 响应数据
* @returns HttpResponse<T> 处理后的响应
*/
handleResponse(response, responseData) {
// 检查是否为业务响应结构
if (this.isBusinessResponse(responseData)) {
// 业务响应结构:{ code, msg, data }
const { code, msg, data } = responseData;
// 检查业务状态码
if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {
// 业务错误抛出HttpError
throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData);
}
// 业务成功返回data字段作为实际数据
return {
status: response.status,
statusText: response.statusText,
data: data,
headers: this.parseHeaders(response.headers)
};
}
// 非业务响应结构,直接返回原始数据
return {
status: response.status,
statusText: response.statusText,
data: responseData,
headers: this.parseHeaders(response.headers)
};
}
/**
* 检查是否为业务响应结构
* @param responseData 响应数据
* @returns boolean 是否为业务响应结构
*/
isBusinessResponse(responseData) {
return typeof responseData === 'object' &&
responseData !== null &&
('code' in responseData) &&
('msg' in responseData) &&
('data' in responseData);
}
/**
* 获取错误信息
* @param responseData 响应数据
* @returns string 错误信息
*/
getErrorMessage(responseData) {
// 如果是业务响应结构
if (this.isBusinessResponse(responseData)) {
return responseData.msg || `Business Error: ${responseData.code}`;
}
// 其他错误结构
return responseData.message || responseData.error || `HTTP Error`;
}
/**
* GET请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async get(url, options) {
return this.request({
method: 'GET',
url,
...options
});
}
/**
* POST请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async post(url, body, options) {
return this.request({
method: 'POST',
url,
body,
...options
});
}
/**
* PUT请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async put(url, body, options) {
return this.request({
method: 'PUT',
url,
body,
...options
});
}
/**
* DELETE请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async delete(url, options) {
return this.request({
method: 'DELETE',
url,
...options
});
}
/**
* PATCH请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async patch(url, body, options) {
return this.request({
method: 'PATCH',
url,
body,
...options
});
}
/**
* 解析响应体
* @param response 响应对象
* @returns Promise<any> 解析后的响应体
*/
async parseResponse(response) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
else if (contentType.includes('text/')) {
return response.text();
}
else {
return response.blob();
}
}
/**
* 解析响应头
* @param headers 响应头对象
* @returns Record<string, string> 解析后的响应头
*/
parseHeaders(headers) {
const result = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
}
//# sourceMappingURL=http.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
/**
* Token管理模块
* 负责Token的存储、获取、刷新和过期处理
*/
import { Storage } from '../utils/storage';
/**
* Token管理类
*/
export declare class TokenManager {
private storage;
/**
* 构造函数
* @param storage 存储实例
* @param httpClient HTTP客户端实例
*/
constructor(storage: Storage);
/**
* 存储Token信息
* @param tokenInfo Token信息
*/
saveToken(tokenInfo: string): void;
/**
* 获取Token信息
* @returns TokenInfo | null Token信息
*/
getToken(): string | null;
/**
* 清除Token信息
*/
clearToken(): void;
}
//# sourceMappingURL=token.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE3C;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAU;IAEzB;;;;OAIG;gBACS,OAAO,EAAE,OAAO;IAI5B;;;OAGG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB;;OAEG;IACH,UAAU,IAAI,IAAI;CAGnB"}

View File

@@ -0,0 +1,38 @@
/**
* Token管理模块
* 负责Token的存储、获取、刷新和过期处理
*/
/**
* Token管理类
*/
export class TokenManager {
/**
* 构造函数
* @param storage 存储实例
* @param httpClient HTTP客户端实例
*/
constructor(storage) {
this.storage = storage;
}
/**
* 存储Token信息
* @param tokenInfo Token信息
*/
saveToken(tokenInfo) {
this.storage.set('token', tokenInfo);
}
/**
* 获取Token信息
* @returns TokenInfo | null Token信息
*/
getToken() {
return this.storage.get('token');
}
/**
* 清除Token信息
*/
clearToken() {
this.storage.remove('token');
}
}
//# sourceMappingURL=token.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/core/token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,OAAO,YAAY;IAGvB;;;;OAIG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,SAAiB;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,UAAU;QACR,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF"}

View File

@@ -0,0 +1,55 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
import { Auth } from '../core/auth';
/**
* 路由守卫选项
*/
export interface RouterGuardOptions {
/**
* 是否需要登录
*/
requiresAuth?: boolean;
/**
* 需要的权限列表
*/
requiredPermissions?: string[];
/**
* 登录后重定向的URL
*/
redirectUri?: string;
/**
* 权限不足时重定向的URL
*/
unauthorizedRedirectUri?: string;
}
/**
* 路由守卫类
*/
export declare class RouterGuard {
private auth;
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth: Auth);
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<boolean> 是否通过权限检查
*/
check(options: RouterGuardOptions): Promise<boolean>;
/**
* 创建Vue路由守卫
* @returns 路由守卫函数
*/
createVueGuard(): (to: any, from: any, next: any) => Promise<void>;
/**
* 检查当前用户是否有权限访问资源
* @param permissions 需要的权限列表
* @returns Promise<boolean> 是否拥有权限
*/
hasPermission(permissions: string | string[]): Promise<boolean>;
}
//# sourceMappingURL=router.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAO;IAEnB;;;OAGG;gBACS,IAAI,EAAE,IAAI;IAItB;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC1D;;;OAGG;IACH,cAAc,KACE,IAAI,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG;IAgB7C;;;;OAIG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAoBtE"}

View File

@@ -0,0 +1,89 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
/**
* 路由守卫类
*/
export class RouterGuard {
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth) {
this.auth = auth;
}
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<boolean> 是否通过权限检查
*/
async check(options) {
const { requiresAuth = true, requiredPermissions = [] } = options;
// 检查是否需要登录
if (requiresAuth) {
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
// 未认证,跳转到登录页
this.auth.login(options.redirectUri);
return false;
}
// 检查是否需要权限
if (requiredPermissions.length > 0) {
// 获取用户权限
const userPermissions = [''];
// 检查是否拥有所有需要的权限
const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission));
if (!hasPermission) {
// 权限不足,跳转到权限不足页
if (options.unauthorizedRedirectUri) {
window.location.href = options.unauthorizedRedirectUri;
}
return false;
}
}
}
return true;
}
/**
* 创建Vue路由守卫
* @returns 路由守卫函数
*/
createVueGuard() {
return async (to, from, next) => {
var _a;
// 从路由元信息中获取守卫选项
const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {};
try {
const allowed = await this.check(options);
if (allowed) {
next();
}
}
catch (error) {
console.error('Route guard error:', error);
next(false);
}
};
}
/**
* 检查当前用户是否有权限访问资源
* @param permissions 需要的权限列表
* @returns Promise<boolean> 是否拥有权限
*/
async hasPermission(permissions) {
if (!permissions) {
return true;
}
const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
return false;
}
// 获取用户权限
const userPermissions = [''];
// 检查是否拥有所有需要的权限
return requiredPermissions.every(permission => userPermissions.includes(permission));
}
}
//# sourceMappingURL=router.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/guards/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA0BH;;GAEG;AACH,MAAM,OAAO,WAAW;IAGtB;;;OAGG;IACH,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,OAA2B;QACrC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,mBAAmB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;QAElE,WAAW;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,UAAU;YACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;gBACjC,aAAa;gBACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBACrC,OAAO,KAAK,CAAC;YACf,CAAC;YAED,WAAW;YACX,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnC,SAAS;gBACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAC;gBAE7B,gBAAgB;gBAChB,MAAM,aAAa,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC3D,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;gBAEF,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,gBAAgB;oBAChB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;wBACpC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,uBAAuB,CAAC;oBACzD,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,KAAK,EAAE,EAAO,EAAE,IAAS,EAAE,IAAS,EAAE,EAAE;;YAC7C,gBAAgB;YAChB,MAAM,OAAO,GAAuB,CAAA,MAAA,EAAE,CAAC,IAAI,0CAAE,IAAI,KAAI,EAAE,CAAC;YAExD,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC1C,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,EAAE,CAAC;gBACT,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;gBAC3C,IAAI,CAAC,KAAK,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,WAA8B;QAChD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QAErF,UAAU;QACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS;QACT,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,CAAA;QAE5B,gBAAgB;QAChB,OAAO,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAC5C,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CACrC,CAAC;IACJ,CAAC;CACF"}

View File

@@ -0,0 +1,20 @@
/**
* 统一登录SDK入口文件
* 支持OAuth2授权码模式提供完整的Token管理和用户信息管理功能
*/
export { Auth } from './core/auth';
export { TokenManager } from './core/token';
export { HttpClient, HttpError } from './core/http';
export { Storage } from './utils/storage';
export { RouterGuard, RouterGuardOptions } from './guards/router';
export { generateRandomString, parseQueryParams, buildQueryParams, generateAuthorizationUrl, isCallbackUrl } from './utils/url';
export * from './types';
export { VuePlugin, createVuePlugin } from './plugins/vue';
import { UnifiedLoginSDK } from './types';
/**
* 默认导出的SDK实例
*/
export declare const unifiedLoginSDK: UnifiedLoginSDK;
export default unifiedLoginSDK;
export declare const version = "1.0.0";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACd,MAAM,aAAa,CAAC;AAGrB,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAa,eAAe,EAAE,MAAM,SAAS,CAAC;AAUrD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,eA8C7B,CAAC;AAGF,eAAe,eAAe,CAAC;AAG/B,eAAO,MAAM,OAAO,UAAU,CAAC"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/**
* Vue插件模块
* 提供Vue应用中使用统一登录SDK的能力
*/
import { Auth } from '../core/auth';
import { SDKConfig } from '../types';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue插件选项
*/
export interface VuePluginOptions {
/**
* SDK配置
*/
config: SDKConfig;
/**
* 插件名称,默认'unifiedLogin'
*/
pluginName?: string;
}
/**
* Vue插件类
*/
export declare class VuePlugin {
private auth;
private routerGuard;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage);
/**
* 安装Vue插件
* @param app Vue构造函数或Vue 3应用实例
* @param options 插件选项
*/
install(app: any, options: VuePluginOptions): void;
/**
* 获取认证实例
* @returns Auth 认证实例
*/
getAuth(): Auth;
/**
* 获取路由守卫实例
* @returns RouterGuard 路由守卫实例
*/
getRouterGuard(): RouterGuard;
}
/**
* 创建Vue插件实例
* @param storageType 存储类型
* @returns VuePlugin Vue插件实例
*/
export declare function createVuePlugin(storageType?: 'localStorage' | 'sessionStorage' | 'cookie'): VuePlugin;
//# sourceMappingURL=vue.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,WAAW,CAAc;IAEjC;;;OAGG;gBACS,OAAO,EAAE,OAAO;IAK5B;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAkDlD;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf;;;OAGG;IACH,cAAc,IAAI,WAAW;CAG9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAGrG"}

View File

@@ -0,0 +1,93 @@
/**
* Vue插件模块
* 提供Vue应用中使用统一登录SDK的能力
*/
import { Auth } from '../core/auth';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue插件类
*/
export class VuePlugin {
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage) {
this.auth = new Auth(storage);
this.routerGuard = new RouterGuard(this.auth);
}
/**
* 安装Vue插件
* @param app Vue构造函数或Vue 3应用实例
* @param options 插件选项
*/
install(app, options) {
const { config, pluginName = 'unifiedLogin' } = options;
// 初始化SDK
this.auth.init(config);
// 判断是Vue 2还是Vue 3
const isVue3 = typeof app.config !== 'undefined';
if (isVue3) {
// Vue 3
// 在全局属性上挂载SDK实例
app.config.globalProperties[`${pluginName}`] = this.auth;
app.config.globalProperties.$auth = this.auth; // 兼容简写
// 提供Vue组件内的注入
app.provide(pluginName, this.auth);
app.provide('auth', this.auth); // 兼容简写
// 处理路由守卫
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
}
else {
// Vue 2
// 在Vue实例上挂载SDK实例
app.prototype[`${pluginName}`] = this.auth;
app.prototype.$auth = this.auth; // 兼容简写
// 全局混入
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
}
}
/**
* 获取认证实例
* @returns Auth 认证实例
*/
getAuth() {
return this.auth;
}
/**
* 获取路由守卫实例
* @returns RouterGuard 路由守卫实例
*/
getRouterGuard() {
return this.routerGuard;
}
}
/**
* 创建Vue插件实例
* @param storageType 存储类型
* @returns VuePlugin Vue插件实例
*/
export function createVuePlugin(storageType) {
const storage = new Storage(storageType);
return new VuePlugin(storage);
}
//# sourceMappingURL=vue.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"vue.js","sourceRoot":"","sources":["../../src/plugins/vue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgB/C;;GAEG;AACH,MAAM,OAAO,SAAS;IAIpB;;;OAGG;IACH,YAAY,OAAgB;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAQ,EAAE,OAAyB;QACzC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;QAExD,SAAS;QACT,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEvB,kBAAkB;QAClB,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAEjD,IAAI,MAAM,EAAE,CAAC;YACX,QAAQ;YACR,gBAAgB;YAChB,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YACzD,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAEtD,cAAc;YACd,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;YAEvC,SAAS;YACT,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ;YACR,iBAAiB;YACjB,GAAG,CAAC,SAAS,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO;YAExC,OAAO;YACP,GAAG,CAAC,KAAK,CAAC;gBACR,YAAY;oBACV,gBAAgB;oBAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACpC,WAAW;wBACX,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAA0D;IACxF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC"}

View File

@@ -0,0 +1,39 @@
/**
* SDK配置选项
*/
export interface SDKConfig {
/** 客户端ID */
clientId: string;
/** 注册id **/
registrationId: string;
/** 后端basepath路径*/
basepath: string;
/** 存储类型默认localStorage */
storageType?: 'localStorage' | 'sessionStorage' | 'cookie';
idpLogoutUrl: string;
homePage: string;
/** 租户ID可选 */
tenantId?: string;
}
/**
* Token信息
*/
export interface TokenInfo {
/** 访问令牌 */
accessToken: string;
/** 刷新令牌 */
refreshToken: string;
/** 令牌类型默认Bearer */
tokenType?: string;
/** 访问令牌过期时间(秒) */
expiresIn: number;
/** 刷新令牌过期时间(秒) */
refreshExpiresIn?: number;
/** 令牌颁发时间戳 */
issuedAt: number;
}
/**
* 事件类型
*/
export type EventType = 'login' | 'logout' | 'tokenExpired';
//# sourceMappingURL=config.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,YAAY;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IAC3D,YAAY,EAAC,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,WAAW;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,80 @@
export * from './config';
export * from './user';
/**
* 统一登录SDK接口
*/
export interface UnifiedLoginSDK {
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: import('./config').SDKConfig): void;
getToken(): string | null;
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
login(redirectUri?: string): Promise<void>;
/**
* 退出登录
*/
logout(): Promise<void>;
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
handleCallback(): Promise<void>;
getRoutes(): Promise<import('./user').RouterInfo>;
/**
* 获取用户信息
* @returns Promise<UserInfo> 用户信息
*/
getUserInfo(): import('./user').UserInfo;
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean;
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
hasRole(role: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
hasAllRoles(roles: string[]): Promise<boolean>;
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
hasPermission(permission: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
hasAllPermissions(permissions: string[]): Promise<boolean>;
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: import('./config').EventType, callback: Function): void;
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: import('./config').EventType, callback: Function): void;
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean;
}
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAEjD,QAAQ,IAAG,MAAM,GAAC,IAAI,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,OAAO,QAAQ,EAAE,UAAU,CAAC,CAAC;IAElD;;;OAGG;IACH,WAAW,IAAI,OAAO,QAAQ,EAAE,QAAQ,CAAC;IAEzC;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEnD;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/C;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/D;;;;OAIG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE3D;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAElE;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAEnE;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC;CACvB"}

View File

@@ -0,0 +1,3 @@
export * from './config';
export * from './user';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC"}

View File

@@ -0,0 +1,83 @@
/**
* 菜单信息
*/
export interface RouterInfo {
/** 菜单名称 */
name: string;
/** 菜单路径 */
path?: string;
hidden: boolean;
redirect: string;
query: string;
alwaysShow: boolean;
/** 菜单组件 */
component?: string;
meta: MetaVo;
children: RouterInfo;
}
export interface MetaVo {
/**
* 设置该路由在侧边栏和面包屑中展示的名字
*/
title: string;
/**
* 设置该路由的图标对应路径src/assets/icons/svg
*/
icon: string;
/**
* 设置为true则不会被 <keep-alive>缓存
*/
noCache: boolean;
/**
* 内链地址http(s)://开头)
*/
link: string;
}
/**
* 用户基本信息
*/
export interface UserInfo {
/** 用户ID */
userId: string;
/** 用户名 */
username: string;
/** 姓名 */
nickName: string;
/** 邮箱 */
currentDeptId: string;
/** 部门 */
userDepts?: UserDept[];
/** 岗位 */
userPost?: UserPost[];
/** 性别 */
sex: string;
/** 用户角色 */
roles?: string[];
/** 权限列表 */
permissions?: string[];
dataPermission: DataPermission;
}
export interface DataPermission {
allowAll: boolean;
onlySelf: boolean;
deptList?: string[];
areas?: string[];
}
export interface UserDept {
postCode: string;
postId: bigint;
postName: string;
postSort: bigint;
remark: string;
status: bigint;
}
export interface UserPost {
ancestors: string;
deptId: bigint;
deptName: string;
leader: string;
orderNum: bigint;
parentId: bigint;
status: bigint;
}
//# sourceMappingURL=user.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAC,MAAM,CAAC;IACZ,QAAQ,EAAE,UAAU,CAAC;CACtB;AACD,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,KAAK,EAAC,MAAM,CAAC;IAEb;;OAEG;IACH,IAAI,EAAC,MAAM,CAAC;IAEZ;;OAEG;IACH,OAAO,EAAC,OAAO,CAAC;IAEhB;;OAEG;IACH,IAAI,EAAC,MAAM,CAAC;CACb;AAGD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,WAAW;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS;IACT,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS;IACT,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,SAAS;IACT,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,SAAS;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW;IACX,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,cAAc,CAAA;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAC,MAAM,EAAE,CAAA;CAChB;AACD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAC,MAAM,CAAA;IACf,MAAM,EAAC,MAAM,CAAA;IACb,QAAQ,EAAC,MAAM,CAAA;IACf,QAAQ,EAAC,MAAM,CAAA;IACf,MAAM,EAAC,MAAM,CAAA;IACb,MAAM,EAAC,MAAM,CAAA;CACd;AACD,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=user.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,108 @@
/**
* 存储工具类
* 支持localStorage、sessionStorage和cookie三种存储方式
*/
type StorageType = 'localStorage' | 'sessionStorage' | 'cookie';
/**
* 存储工具类
*/
export declare class Storage {
private storageType;
private prefix;
/**
* 构造函数
* @param storageType 存储类型
* @param prefix 存储前缀,默认'unified_login_'
*/
constructor(storageType?: StorageType, prefix?: string);
/**
* 设置存储项
* @param key 存储键
* @param value 存储值
* @param options 可选参数cookie存储时使用
*/
set(key: string, value: any, options?: {
expires?: number;
path?: string;
domain?: string;
secure?: boolean;
}): void;
/**
* 获取存储项
* @param key 存储键
* @returns 存储值
*/
get(key: string): any;
/**
* 移除存储项
* @param key 存储键
*/
remove(key: string): void;
/**
* 清空所有存储项
*/
clear(): void;
/**
* 检查存储类型是否可用
* @returns boolean 是否可用
*/
isAvailable(): boolean;
/**
* 设置localStorage
*/
private setLocalStorage;
/**
* 获取localStorage
*/
private getLocalStorage;
/**
* 移除localStorage
*/
private removeLocalStorage;
/**
* 清空localStorage中所有带前缀的项
*/
private clearLocalStorage;
/**
* 检查localStorage是否可用
*/
private isLocalStorageAvailable;
/**
* 设置sessionStorage
*/
private setSessionStorage;
/**
* 获取sessionStorage
*/
private getSessionStorage;
/**
* 移除sessionStorage
*/
private removeSessionStorage;
/**
* 清空sessionStorage中所有带前缀的项
*/
private clearSessionStorage;
/**
* 检查sessionStorage是否可用
*/
private isSessionStorageAvailable;
/**
* 设置cookie
*/
private setCookie;
/**
* 获取cookie
*/
private getCookie;
/**
* 移除cookie
*/
private removeCookie;
/**
* 清空所有带前缀的cookie
*/
private clearCookie;
}
export {};
//# sourceMappingURL=storage.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,WAAW,GAAG,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEhE;;GAEG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAS;IAEvB;;;;OAIG;gBACS,WAAW,GAAE,WAA4B,EAAE,MAAM,GAAE,MAAyB;IAKxF;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAiBpH;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG;IA+BrB;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAgBzB;;OAEG;IACH,KAAK,IAAI,IAAI;IAcb;;;OAGG;IACH,WAAW,IAAI,OAAO;IAmBtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAgB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAM5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAgBjC;;OAEG;IACH,OAAO,CAAC,SAAS;IAsCjB;;OAEG;IACH,OAAO,CAAC,SAAS;IAsBjB;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,WAAW;CAcpB"}

View File

@@ -0,0 +1,316 @@
/**
* 存储工具类
* 支持localStorage、sessionStorage和cookie三种存储方式
*/
/**
* 存储工具类
*/
export class Storage {
/**
* 构造函数
* @param storageType 存储类型
* @param prefix 存储前缀,默认'unified_login_'
*/
constructor(storageType = 'localStorage', prefix = 'unified_login_') {
this.storageType = storageType;
this.prefix = prefix;
}
/**
* 设置存储项
* @param key 存储键
* @param value 存储值
* @param options 可选参数cookie存储时使用
*/
set(key, value, options) {
const fullKey = this.prefix + key;
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
switch (this.storageType) {
case 'localStorage':
this.setLocalStorage(fullKey, stringValue);
break;
case 'sessionStorage':
this.setSessionStorage(fullKey, stringValue);
break;
case 'cookie':
this.setCookie(fullKey, stringValue, options);
break;
}
}
/**
* 获取存储项
* @param key 存储键
* @returns 存储值
*/
get(key) {
const fullKey = this.prefix + key;
let value;
switch (this.storageType) {
case 'localStorage':
value = this.getLocalStorage(fullKey);
break;
case 'sessionStorage':
value = this.getSessionStorage(fullKey);
break;
case 'cookie':
value = this.getCookie(fullKey);
break;
default:
value = null;
}
if (value === null) {
return null;
}
// 尝试解析JSON
try {
return JSON.parse(value);
}
catch (e) {
// 如果不是JSON直接返回字符串
return value;
}
}
/**
* 移除存储项
* @param key 存储键
*/
remove(key) {
const fullKey = this.prefix + key;
switch (this.storageType) {
case 'localStorage':
this.removeLocalStorage(fullKey);
break;
case 'sessionStorage':
this.removeSessionStorage(fullKey);
break;
case 'cookie':
this.removeCookie(fullKey);
break;
}
}
/**
* 清空所有存储项
*/
clear() {
switch (this.storageType) {
case 'localStorage':
this.clearLocalStorage();
break;
case 'sessionStorage':
this.clearSessionStorage();
break;
case 'cookie':
this.clearCookie();
break;
}
}
/**
* 检查存储类型是否可用
* @returns boolean 是否可用
*/
isAvailable() {
try {
switch (this.storageType) {
case 'localStorage':
return this.isLocalStorageAvailable();
case 'sessionStorage':
return this.isSessionStorageAvailable();
case 'cookie':
return typeof document !== 'undefined';
default:
return false;
}
}
catch (e) {
return false;
}
}
// ------------------------ localStorage 操作 ------------------------
/**
* 设置localStorage
*/
setLocalStorage(key, value) {
if (this.isLocalStorageAvailable()) {
localStorage.setItem(key, value);
}
}
/**
* 获取localStorage
*/
getLocalStorage(key) {
if (this.isLocalStorageAvailable()) {
return localStorage.getItem(key);
}
return null;
}
/**
* 移除localStorage
*/
removeLocalStorage(key) {
if (this.isLocalStorageAvailable()) {
localStorage.removeItem(key);
}
}
/**
* 清空localStorage中所有带前缀的项
*/
clearLocalStorage() {
if (this.isLocalStorageAvailable()) {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(this.prefix)) {
localStorage.removeItem(key);
i--; // 索引调整
}
}
}
}
/**
* 检查localStorage是否可用
*/
isLocalStorageAvailable() {
if (typeof localStorage === 'undefined') {
return false;
}
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
}
catch (e) {
return false;
}
}
// ------------------------ sessionStorage 操作 ------------------------
/**
* 设置sessionStorage
*/
setSessionStorage(key, value) {
if (this.isSessionStorageAvailable()) {
sessionStorage.setItem(key, value);
}
}
/**
* 获取sessionStorage
*/
getSessionStorage(key) {
if (this.isSessionStorageAvailable()) {
return sessionStorage.getItem(key);
}
return null;
}
/**
* 移除sessionStorage
*/
removeSessionStorage(key) {
if (this.isSessionStorageAvailable()) {
sessionStorage.removeItem(key);
}
}
/**
* 清空sessionStorage中所有带前缀的项
*/
clearSessionStorage() {
if (this.isSessionStorageAvailable()) {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith(this.prefix)) {
sessionStorage.removeItem(key);
i--; // 索引调整
}
}
}
}
/**
* 检查sessionStorage是否可用
*/
isSessionStorageAvailable() {
if (typeof sessionStorage === 'undefined') {
return false;
}
try {
const testKey = '__storage_test__';
sessionStorage.setItem(testKey, testKey);
sessionStorage.removeItem(testKey);
return true;
}
catch (e) {
return false;
}
}
// ------------------------ cookie 操作 ------------------------
/**
* 设置cookie
*/
setCookie(key, value, options) {
if (typeof document === 'undefined') {
return;
}
let cookieString = `${key}=${encodeURIComponent(value)}`;
if (options) {
// 设置过期时间(秒)
if (options.expires) {
const date = new Date();
date.setTime(date.getTime() + options.expires * 1000);
cookieString += `; expires=${date.toUTCString()}`;
}
// 设置路径
if (options.path) {
cookieString += `; path=${options.path}`;
}
// 设置域名
if (options.domain) {
cookieString += `; domain=${options.domain}`;
}
// 设置secure
if (options.secure) {
cookieString += '; secure';
}
}
document.cookie = cookieString;
}
/**
* 获取cookie
*/
getCookie(key) {
if (typeof document === 'undefined') {
return null;
}
const name = `${key}=`;
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return null;
}
/**
* 移除cookie
*/
removeCookie(key) {
this.setCookie(key, '', { expires: -1 });
}
/**
* 清空所有带前缀的cookie
*/
clearCookie() {
if (typeof document === 'undefined') {
return;
}
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const eqPos = cookie.indexOf('=');
const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
if (key.startsWith(this.prefix)) {
this.removeCookie(key);
}
}
}
}
//# sourceMappingURL=storage.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
/**
* URL处理工具
* 用于生成授权URL、解析URL参数等功能
*/
/**
* 生成随机字符串
* @param length 字符串长度默认32位
* @returns 随机字符串
*/
export declare function generateRandomString(length?: number): string;
/**
* 解析URL查询参数
* @param url URL字符串默认为当前URL
* @returns 查询参数对象
*/
export declare function parseQueryParams(url?: string): Record<string, string>;
/**
* 构建URL查询参数
* @param params 查询参数对象
* @returns 查询参数字符串
*/
export declare function buildQueryParams(params: Record<string, any>): string;
/**
* 生成OAuth2授权URL
* @param authorizationEndpoint 授权端点URL
* @param clientId 客户端ID
* @param redirectUri 重定向URL
* @param options 可选参数
* @returns 授权URL
*/
export declare function generateAuthorizationUrl(authorizationEndpoint: string, clientId: string, redirectUri: string, options?: {
responseType?: string;
scope?: string;
state?: string;
[key: string]: any;
}): string;
/**
* 检查当前URL是否为授权回调
* @param url URL字符串默认为当前URL
* @returns 是否为授权回调
*/
export declare function isCallbackUrl(url?: string): boolean;
/**
* 获取当前URL的路径名
* @param url URL字符串默认为当前URL
* @returns 路径名
*/
export declare function getPathname(url?: string): string;
/**
* 获取当前URL的主机名
* @param url URL字符串默认为当前URL
* @returns 主机名
*/
export declare function getHostname(url?: string): string;
//# sourceMappingURL=url.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAOhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB3F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQpE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,qBAAqB,EAAE,MAAM,EAC7B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,GACA,MAAM,CAmBR;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAA6B,GAAG,OAAO,CAGzE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAGtE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAA6B,GAAG,MAAM,CAGtE"}

View File

@@ -0,0 +1,100 @@
/**
* URL处理工具
* 用于生成授权URL、解析URL参数等功能
*/
/**
* 生成随机字符串
* @param length 字符串长度默认32位
* @returns 随机字符串
*/
export function generateRandomString(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 解析URL查询参数
* @param url URL字符串默认为当前URL
* @returns 查询参数对象
*/
export function parseQueryParams(url = window.location.href) {
const params = {};
const queryString = url.split('?')[1];
if (!queryString) {
return params;
}
const pairs = queryString.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key) {
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
}
}
return params;
}
/**
* 构建URL查询参数
* @param params 查询参数对象
* @returns 查询参数字符串
*/
export function buildQueryParams(params) {
const pairs = [];
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
}
return pairs.length ? `?${pairs.join('&')}` : '';
}
/**
* 生成OAuth2授权URL
* @param authorizationEndpoint 授权端点URL
* @param clientId 客户端ID
* @param redirectUri 重定向URL
* @param options 可选参数
* @returns 授权URL
*/
export function generateAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, options) {
const { responseType = 'code', scope, state = generateRandomString(32), ...extraParams } = options || {};
const params = {
client_id: clientId,
redirect_uri: redirectUri,
response_type: responseType,
state,
...(scope ? { scope } : {}),
...extraParams
};
const queryString = buildQueryParams(params);
return `${authorizationEndpoint}${queryString}`;
}
/**
* 检查当前URL是否为授权回调
* @param url URL字符串默认为当前URL
* @returns 是否为授权回调
*/
export function isCallbackUrl(url = window.location.href) {
const params = parseQueryParams(url);
return !!params.code || !!params.error;
}
/**
* 获取当前URL的路径名
* @param url URL字符串默认为当前URL
* @returns 路径名
*/
export function getPathname(url = window.location.href) {
const urlObj = new URL(url);
return urlObj.pathname;
}
/**
* 获取当前URL的主机名
* @param url URL字符串默认为当前URL
* @returns 主机名
*/
export function getHostname(url = window.location.href) {
const urlObj = new URL(url);
return urlObj.hostname;
}
//# sourceMappingURL=url.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE;IACtD,MAAM,KAAK,GAAG,gEAAgE,CAAC;IAC/E,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,MAAM,CAAC,QAAQ,CAAC,IAAI;IACjE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,GAAG,kBAAkB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA2B;IAC1D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CACtC,qBAA6B,EAC7B,QAAgB,EAChB,WAAmB,EACnB,OAKC;IAED,MAAM,EACJ,YAAY,GAAG,MAAM,EACrB,KAAK,EACL,KAAK,GAAG,oBAAoB,CAAC,EAAE,CAAC,EAChC,GAAG,WAAW,EACf,GAAG,OAAO,IAAI,EAAE,CAAC;IAElB,MAAM,MAAM,GAAG;QACb,SAAS,EAAE,QAAQ;QACnB,YAAY,EAAE,WAAW;QACzB,aAAa,EAAE,YAAY;QAC3B,KAAK;QACL,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,GAAG,WAAW;KACf,CAAC;IAEF,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC7C,OAAO,GAAG,qBAAqB,GAAG,WAAW,EAAE,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,MAAM,CAAC,QAAQ,CAAC,IAAI;IAC9D,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACrC,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,MAAM,CAAC,QAAQ,CAAC,IAAI;IAC5D,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,MAAM,CAAC,QAAQ,CAAC,IAAI;IAC5D,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC;AACzB,CAAC"}

View File

@@ -0,0 +1,41 @@
{
"name": "unified-login-sdk",
"version": "1.0.0",
"description": "TypeScript前端SDK用于前后端分离项目对接统一登录系统",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc && rollup -c",
"dev": "tsc -w",
"test": "jest",
"lint": "eslint src --ext .ts",
"prepublishOnly": "npm run build"
},
"keywords": [
"oauth2",
"login",
"sdk",
"typescript",
"unified-login"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.16",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.56.0",
"jest-environment-jsdom": "^30.2.0",
"rollup": "^4.9.6",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "^5.3.3"
},
"files": [
"dist",
"src"
],
"engines": {
"node": ">=16.0.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
import typescript from 'rollup-plugin-typescript2';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
}
],
plugins: [
typescript({
tsconfig: './tsconfig.json',
clean: true
})
]
};

View File

@@ -0,0 +1,340 @@
/**
* 认证核心逻辑
* 实现OAuth2授权码模式的完整流程
*/
import {EventType, RouterInfo, SDKConfig, UserInfo} from '../types';
import {TokenManager} from './token';
import {HttpClient} from './http';
import {Storage} from '../utils/storage';
import {buildQueryParams, isCallbackUrl, parseQueryParams} from '../utils/url';
/**
* 认证核心类
*/
export class Auth {
private config: SDKConfig | null = null;
private tokenManager!: TokenManager;
private httpClient: HttpClient;
private readonly storage: Storage;
private eventHandlers: Record<EventType, Function[]> = {
login: [],
logout: [],
tokenExpired: []
};
private userInfoCache: UserInfo | null = null;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage) {
this.storage = storage;
// 创建带有基础配置的HttpClient
this.httpClient = new HttpClient({
timeout: 15000,
withCredentials: true
});
}
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: SDKConfig): void {
this.config = config;
// 更新HTTP客户端配置
this.httpClient.updateConfig({
baseURL: config.basepath || '',
tenantId: config.tenantId
});
// 创建TokenManager
this.tokenManager = new TokenManager(this.storage, this.config.clientId);
// 设置Token获取函数
this.httpClient.setTokenGetter(() => this.getToken());
}
getToken():string | null{
return this.tokenManager.getToken()
}
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
async login(redirectUri?: string): Promise<void> {
if (!this.config) {
throw new Error('SDK not initialized');
}
const registrationId = this.config.registrationId || 'idp'
const path = `/oauth2/authorization/${registrationId}`
const tokenResponse = await this.httpClient.get(path,{needAuth:false})
const redirect = tokenResponse.data.redirect_url
const params = parseQueryParams(redirect)
// 安全存储当前页面URL用于回调后重定向
if (params.state) {
// 确保存储的URL是有效的
const currentUrl = redirectUri || window.location.href;
const safeUrl = currentUrl && typeof currentUrl === 'string' ? currentUrl : '/';
this.storage.set(params.state, safeUrl);
console.log('💾 存储重定向状态:', params.state, '->', safeUrl);
}
window.location.href = redirect
}
/**
* 退出登录
*/
async logout(): Promise<void> {
if (!this.config) {
throw new Error('SDK not initialized');
}
// 清除本地存储的Token和用户信息缓存
this.tokenManager.clearToken();
this.userInfoCache = null;
await this.httpClient.post(`/logout`,null,{needAuth:true})
// 触发退出事件
this.emit('logout');
window.location.href = this.config.idpLogoutUrl+'?redirect='+this.config.homePage;
}
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
async handleCallback(): Promise<void> {
if (!this.config) {
throw new Error('SDK not initialized');
}
const params = parseQueryParams();
// 检查是否有错误
if (params.error) {
throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);
}
// 检查是否有授权码
if (!params.code) {
throw new Error('Authorization code not found');
}
const registrationId = this.config.registrationId || 'idp'
const callback = `/login/oauth2/code/${registrationId}${buildQueryParams(params)}`
const tokenResponse = await this.httpClient.get(callback,{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
needAuth: false
})
// 触发登录事件
this.emit('login');
// 缓存用户信息
this.userInfoCache = tokenResponse.data.data;
this.tokenManager.saveToken(tokenResponse.headers['authorization']||tokenResponse.headers['Authorization'])
// 安全处理重定向URL
let redirectUrl = this.config.homePage || '/';
if (params.state) {
const storedUrl = this.storage.get(params.state);
if (storedUrl && typeof storedUrl === 'string') {
redirectUrl = storedUrl;
}
this.storage.remove(params.state);
}
// 确保URL格式正确
if (!redirectUrl.startsWith('http') && !redirectUrl.startsWith('/')) {
redirectUrl = '/' + redirectUrl;
}
console.log('🔄 重定向到:', redirectUrl);
window.location.href = redirectUrl;
}
async getRoutes(): Promise<RouterInfo> {
if (!this.config) {
throw new Error('SDK not initialized');
}
const tokenResponse = await this.httpClient.get(`/idp/routes`,{needAuth:true})
if(tokenResponse.status===401){
await this.logout()
}
return tokenResponse.data.data
}
/**
* 刷新用户信息缓存
* @returns Promise<UserInfo> 更新后的用户信息
*/
async refreshUserInfo(): Promise<UserInfo> {
console.log('🔄 刷新用户信息缓存...');
this.userInfoCache = null; // 清除缓存
return await this.getUserInfo(); // 重新获取
}
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
async getUserInfo(): Promise<UserInfo> {
// 首先检查缓存
if (this.userInfoCache) {
console.log('📋 从缓存获取用户信息');
return this.userInfoCache;
}
// 检查是否已认证
if (!this.isAuthenticated()) {
throw new Error('User not authenticated');
}
try {
// 从后端接口获取用户信息
console.log('🌐 从后端获取用户信息...');
const response = await this.httpClient.get('/idp/getUserInfo', { needAuth: true });
this.userInfoCache = response.data;
console.log('✅ 用户信息获取成功');
return this.userInfoCache!;
} catch (error) {
console.error('❌ 获取用户信息失败:', error);
throw error;
}
}
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
async hasRole(role: string | string[]): Promise<boolean> {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = await this.getUserInfo();
const roleCodes = userInfo.roles||[];
if (Array.isArray(role)) {
// 检查是否有任一角色
return role.some(r => roleCodes.includes(r));
}
// 检查是否有单个角色
return roleCodes.includes(role);
}
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
async hasAllRoles(roles: string[]): Promise<boolean> {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = await this.getUserInfo();
const roleCodes = userInfo.roles||[];
// 检查是否有所有角色
return roles.every(r => roleCodes.includes(r));
}
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
async hasPermission(permission: string | string[]): Promise<boolean> {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = await this.getUserInfo();
const permissions = userInfo.permissions||[];
if (Array.isArray(permission)) {
// 检查是否有任一权限
return permission.some(p => permissions.includes(p));
}
// 检查是否有单个权限
return permissions.includes(permission);
}
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
async hasAllPermissions(permissions: string[]): Promise<boolean> {
if (!this.isAuthenticated()) {
return false;
}
const userInfo = await this.getUserInfo();
const userPermissions = userInfo.permissions||[];
// 检查是否有所有权限
return permissions.every(p => userPermissions.includes(p));
}
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean {
// 检查Token是否存在且未过期
return !!this.tokenManager.getToken();
}
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: EventType, callback: Function): void {
this.eventHandlers[event].push(callback);
}
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: EventType, callback: Function): void {
this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);
}
/**
* 触发事件
* @param event 事件类型
* @param data 事件数据
*/
private emit(event: EventType, data?: any): void {
this.eventHandlers[event].forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`Error in ${event} event handler:`, error);
}
});
}
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean {
return isCallbackUrl();
}
}

View File

@@ -0,0 +1,287 @@
/**
* HTTP客户端使用示例
*/
import { HttpClient, RequestInterceptor, ResponseInterceptor } from './http';
import { Storage } from '../utils/storage';
// 基础使用示例
export function basicUsageExample() {
// 创建HTTP客户端实例
const httpClient = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 10000,
withCredentials: true
});
// 设置Token获取函数
httpClient.setTokenGetter(() => {
// 从TokenManager或其他地方获取Token
return localStorage.getItem('auth_token') || '';
});
// 设置租户ID
httpClient.setTenantId('tenant-123');
return httpClient;
}
// 拦截器使用示例
export function interceptorExample() {
const httpClient = new HttpClient();
// 请求拦截器 - 添加通用头信息
const authInterceptor: RequestInterceptor = {
onRequest: (options) => {
// 添加认证头
const token = localStorage.getItem('auth_token');
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
// 添加请求ID用于追踪
options.headers = {
...options.headers,
'X-Request-ID': Math.random().toString(36).substr(2, 9)
};
return options;
},
onRequestError: (error) => {
console.error('请求拦截器错误:', error);
return Promise.reject(error);
}
};
// 响应拦截器 - 统一处理响应格式
const responseInterceptor: ResponseInterceptor = {
onResponse: (response) => {
// 记录响应时间
console.log(`Response received: ${response.status} ${response.statusText}`);
// 可以在这里统一处理某些业务逻辑
return response;
},
onResponseError: (error) => {
// 统一错误处理
if (error.status === 401) {
// Token过期跳转到登录页
window.location.href = '/login';
}
console.error('Response error:', error.message);
return Promise.reject(error);
}
};
// 添加拦截器
httpClient.addRequestInterceptor(authInterceptor);
httpClient.addResponseInterceptor(responseInterceptor);
return httpClient;
}
// 工厂方法使用示例
export function factoryMethodExample() {
// 创建不同环境的客户端实例
const devClient = HttpClient.create('https://dev-api.example.com', {
timeout: 5000,
withCredentials: true
});
const prodClient = HttpClient.create('https://api.example.com', {
timeout: 15000,
withCredentials: true
});
return { devClient, prodClient };
}
// 配置管理示例
export function configManagementExample() {
const httpClient = new HttpClient({
baseURL: 'https://api.example.com'
});
// 获取当前配置
const currentConfig = httpClient.getConfig();
console.log('Current config:', currentConfig);
// 动态更新配置
httpClient.updateConfig({
timeout: 20000,
headers: {
'X-API-Version': 'v2'
}
});
// 验证配置更新
const updatedConfig = httpClient.getConfig();
console.log('Updated config:', updatedConfig);
return httpClient;
}
// 实际API调用示例
export async function apiCallExample() {
const httpClient = basicUsageExample();
try {
// GET请求
const getUsers = await httpClient.get('/users', {
needAuth: true,
timeout: 8000
});
console.log('Users:', getUsers.data);
// POST请求
const createUser = await httpClient.post('/users', {
name: 'John Doe',
email: 'john@example.com'
}, {
needAuth: true
});
console.log('Created user:', createUser.data);
// PUT请求
const updateUser = await httpClient.put('/users/1', {
name: 'Jane Doe'
});
console.log('Updated user:', updateUser.data);
// DELETE请求
const deleteUser = await httpClient.delete('/users/1');
console.log('Delete result:', deleteUser.data);
} catch (error) {
if (error instanceof Error) {
console.error('API call failed:', error.message);
}
}
}
// 错误处理示例
export function errorHandlingExample() {
// 不同类型的错误处理
const handleHttpError = (error: any) => {
if (error instanceof Error) {
if ('status' in error) {
// HTTP错误
const httpError = error as any;
switch (httpError.status) {
case 401:
console.log('未授权,请重新登录');
break;
case 403:
console.log('禁止访问');
break;
case 404:
console.log('资源不存在');
break;
case 500:
console.log('服务器内部错误');
break;
default:
console.log(`HTTP错误: ${httpError.status} - ${httpError.message}`);
}
} else {
// 网络错误或其他错误
console.log('网络错误:', error.message);
}
}
};
return handleHttpError;
}
// 完整的实际应用示例
export class ApiService {
private httpClient: HttpClient;
constructor() {
this.httpClient = new HttpClient({
baseURL: process.env.API_BASE_URL || 'https://api.example.com',
timeout: 15000
});
// 设置全局拦截器
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.httpClient.addRequestInterceptor({
onRequest: (options) => {
// 添加时间戳防止缓存
if (options.method === 'GET') {
const separator = options.url.includes('?') ? '&' : '?';
options.url += `${separator}_t=${Date.now()}`;
}
return options;
}
});
// 响应拦截器
this.httpClient.addResponseInterceptor({
onResponse: (response) => {
// 统一日志记录
console.log(`[${response.status}] ${response.statusText}`);
return response;
},
onResponseError: (error) => {
// 统一错误上报
console.error('API Error:', {
status: error.status,
message: error.message,
url: window.location.href
});
return Promise.reject(error);
}
});
}
// 用户相关API
async getUsers(page: number = 1, size: number = 10) {
const queryParams = new URLSearchParams({
page: page.toString(),
size: size.toString()
});
return this.httpClient.get(`/users?${queryParams}`);
}
async getUserById(id: string) {
return this.httpClient.get(`/users/${id}`);
}
async createUser(userData: any) {
return this.httpClient.post('/users', userData);
}
async updateUser(id: string, userData: any) {
return this.httpClient.put(`/users/${id}`, userData);
}
async deleteUser(id: string) {
return this.httpClient.delete(`/users/${id}`);
}
// 认证相关API
async login(credentials: { username: string; password: string }) {
return this.httpClient.post('/auth/login', credentials, {
needAuth: false
});
}
async refreshToken(refreshToken: string) {
return this.httpClient.post('/auth/refresh', { refreshToken }, {
needAuth: false
});
}
async logout() {
return this.httpClient.post('/auth/logout');
}
}

View File

@@ -0,0 +1,136 @@
/**
* HTTP客户端测试文件
*/
import { HttpClient, HttpError, RequestInterceptor, ResponseInterceptor } from './http';
import { Storage } from '../utils/storage';
describe('HttpClient', () => {
let httpClient: HttpClient;
let storage: Storage;
beforeEach(() => {
storage = new Storage('localStorage');
httpClient = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 5000
});
});
describe('构造函数', () => {
test('应该正确初始化默认配置', () => {
const client = new HttpClient();
expect(client.getConfig()).toMatchObject({
baseURL: '',
timeout: 10000,
withCredentials: true
});
});
test('应该正确合并自定义配置', () => {
const config = {
baseURL: 'https://test.com',
timeout: 3000,
withCredentials: false
};
const client = new HttpClient(config);
expect(client.getConfig()).toMatchObject(config);
});
});
describe('Token管理', () => {
test('应该能够设置和获取Token获取函数', () => {
const tokenGetter = () => 'test-token';
httpClient.setTokenGetter(tokenGetter);
// 这里需要通过反射或其他方式验证内部状态
// 由于是私有属性,我们通过实际请求来间接测试
});
test('应该能够设置租户ID', () => {
httpClient.setTenantId('test-tenant');
expect(httpClient.getConfig().tenantId).toBe('test-tenant');
});
});
describe('拦截器', () => {
test('应该能够添加和移除请求拦截器', () => {
const interceptor: RequestInterceptor = {
onRequest: (options) => {
options.headers = { ...options.headers, 'X-Custom': 'test' };
return options;
}
};
httpClient.addRequestInterceptor(interceptor);
// 测试逻辑需要配合实际请求
httpClient.removeRequestInterceptor(interceptor);
});
test('应该能够添加和移除响应拦截器', () => {
const interceptor: ResponseInterceptor = {
onResponse: (response) => {
return { ...response, data: { ...response.data, intercepted: true } };
}
};
httpClient.addResponseInterceptor(interceptor);
// 测试逻辑需要配合实际请求
httpClient.removeResponseInterceptor(interceptor);
});
});
describe('静态方法', () => {
test('应该能够创建带基础URL的客户端实例', () => {
const client = HttpClient.create('https://api.test.com', {
timeout: 8000
});
expect(client.getConfig().baseURL).toBe('https://api.test.com');
expect(client.getConfig().timeout).toBe(8000);
});
});
describe('配置管理', () => {
test('应该能够获取和更新配置', () => {
const initialConfig = httpClient.getConfig();
expect(initialConfig.timeout).toBe(5000);
httpClient.updateConfig({ timeout: 8000 });
const updatedConfig = httpClient.getConfig();
expect(updatedConfig.timeout).toBe(8000);
});
});
describe('HTTP错误处理', () => {
test('HttpError应该正确初始化', () => {
const error = new HttpError(
'Test error',
404,
'Not Found',
{ detail: 'Resource not found' },
'RESOURCE_NOT_FOUND'
);
expect(error.message).toBe('Test error');
expect(error.status).toBe(404);
expect(error.statusText).toBe('Not Found');
expect(error.data).toEqual({ detail: 'Resource not found' });
expect(error.code).toBe('RESOURCE_NOT_FOUND');
expect(error.name).toBe('HttpError');
});
test('HttpError应该有正确的堆栈跟踪', () => {
const error = new HttpError('Test error');
expect(error.stack).toBeDefined();
});
});
});
describe('工具函数', () => {
// 这里可以添加对私有方法的测试(如果需要的话)
// 通常通过公共接口间接测试私有逻辑
});

View File

@@ -0,0 +1,655 @@
/**
* HTTP客户端
* 用于与后端API进行通信支持拦截器、超时控制等功能
*/
/**
* HTTP请求方法类型
*/
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
/**
* HTTP客户端配置
*/
export interface HttpClientConfig {
/** 基础URL */
baseURL?: string;
/** 默认请求头 */
headers?: Record<string, string>;
/** 超时时间(毫秒) */
timeout?: number;
/** 是否携带凭证 */
withCredentials?: boolean;
/** 租户ID */
tenantId?: string;
}
/**
* HTTP请求选项
*/
export interface HttpRequestOptions {
/** 请求方法 */
method: HttpMethod;
/** 请求URL */
url: string;
/** 请求头 */
headers?: Record<string, string>;
/** 请求体 */
body?: any;
/** 是否需要认证 */
needAuth?: boolean;
/** 超时时间 */
timeout?: number;
/** 是否携带凭证 */
withCredentials?: boolean;
}
/**
* HTTP响应类型
*/
export interface HttpResponse<T = any> {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 响应体 */
data: T;
/** 响应头 */
headers: Record<string, string>;
}
/**
* 请求拦截器
*/
export interface RequestInterceptor {
/** 请求前拦截 */
onRequest?: (options: HttpRequestOptions) => HttpRequestOptions | Promise<HttpRequestOptions>;
/** 请求错误拦截 */
onRequestError?: (error: any) => any;
}
/**
* 响应拦截器
*/
export interface ResponseInterceptor<T = any> {
/** 响应后拦截 */
onResponse?: (response: HttpResponse<T>) => HttpResponse<T> | Promise<HttpResponse<T>>;
/** 响应错误拦截 */
onResponseError?: (error: HttpError) => any;
}
/**
* HTTP错误类型
*/
export class HttpError extends Error {
/** 状态码 */
public status: number;
/** 状态文本 */
public statusText: string;
/** 错误数据 */
public data: any;
/** 错误代码 */
public code?: string;
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
* @param code 错误代码
*/
constructor(
message: string,
status: number = 0,
statusText: string = 'Unknown Error',
data: any = null,
code?: string
) {
super(message);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.data = data;
this.code = code;
// 保持堆栈跟踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError);
}
}
}
/**
* HTTP客户端类
*/
export class HttpClient {
private config: Required<HttpClientConfig>;
private tokenGetter?: () => string | null;
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor[] = [];
/**
* 构造函数
* @param config 客户端配置
*/
constructor(config: HttpClientConfig = {}) {
this.config = {
baseURL: '',
headers: {
'Content-Type': 'application/json'
},
timeout: 10000,
withCredentials: true,
tenantId: '',
...config
};
}
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter: () => string | null): void {
this.tokenGetter = tokenGetter;
}
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId?: string): void {
this.config.tenantId = tenantId || '';
}
/**
* 添加请求拦截器
* @param interceptor 请求拦截器
*/
addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
}
/**
* 添加响应拦截器
* @param interceptor 响应拦截器
*/
addResponseInterceptor(interceptor: ResponseInterceptor): void {
this.responseInterceptors.push(interceptor);
}
/**
* 移除请求拦截器
* @param interceptor 要移除的拦截器
*/
removeRequestInterceptor(interceptor: RequestInterceptor): void {
const index = this.requestInterceptors.indexOf(interceptor);
if (index > -1) {
this.requestInterceptors.splice(index, 1);
}
}
/**
* 移除响应拦截器
* @param interceptor 要移除的拦截器
*/
removeResponseInterceptor(interceptor: ResponseInterceptor): void {
const index = this.responseInterceptors.indexOf(interceptor);
if (index > -1) {
this.responseInterceptors.splice(index, 1);
}
}
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
// 合并配置
const mergedOptions = this.mergeOptions(options);
try {
// 执行请求拦截器
let processedOptions = await this.executeRequestInterceptors(mergedOptions);
// 构建完整URL
const fullUrl = this.buildFullUrl(processedOptions.url);
// 构建请求配置
const fetchOptions = this.buildFetchOptions(processedOptions);
// 发送请求(包含超时控制)
const response = await this.fetchWithTimeout(
fullUrl,
fetchOptions,
processedOptions.timeout ?? this.config.timeout
);
const responseData = await this.parseResponse(response);
// 构建响应对象
let httpResponse: HttpResponse<T> = {
status: response.status,
statusText: response.statusText,
data: responseData,
headers: this.parseHeaders(response.headers)
};
// 处理业务逻辑
httpResponse = this.handleBusinessLogic(httpResponse);
// 执行响应拦截器
httpResponse = await this.executeResponseInterceptors(httpResponse);
return httpResponse;
} catch (error) {
// 执行响应错误拦截器
return this.executeResponseErrorInterceptors(error);
}
}
/**
* 合并请求选项与默认配置
* @param options 请求选项
* @returns 合并后的选项
*/
private mergeOptions(options: HttpRequestOptions): HttpRequestOptions {
return {
method: options.method,
url: options.url,
headers: {
...this.config.headers,
...options.headers
},
body: options.body,
needAuth: options.needAuth ?? true,
timeout: options.timeout ?? this.config.timeout,
withCredentials: options.withCredentials ?? this.config.withCredentials
};
}
/**
* 构建完整URL
* @param url 相对URL或绝对URL
* @returns 完整URL
*/
private buildFullUrl(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
return this.config.baseURL ? `${this.config.baseURL}${url}` : url;
}
/**
* 构建fetch选项
* @param options 处理后的请求选项
* @returns fetch配置
*/
private buildFetchOptions(options: HttpRequestOptions): RequestInit {
const requestHeaders: Record<string, string> = { ...options.headers };
// 添加认证头
if (options.needAuth && this.tokenGetter) {
const token = this.tokenGetter();
if (token) {
requestHeaders.Authorization = token;
}
}
// 添加租户ID头
if (this.config.tenantId) {
requestHeaders['tenant-id'] = this.config.tenantId;
}
const fetchOptions: RequestInit = {
method: options.method,
headers: requestHeaders,
credentials: options.withCredentials ? 'include' : 'omit'
};
// 添加请求体
if (options.body && ['POST', 'PUT', 'PATCH'].includes(options.method)) {
fetchOptions.body = typeof options.body === 'string'
? options.body
: JSON.stringify(options.body);
}
return fetchOptions;
}
/**
* 带超时控制的fetch
* @param url 请求URL
* @param options fetch选项
* @param timeout 超时时间
* @returns Promise<Response>
*/
private async fetchWithTimeout(
url: string,
options: RequestInit,
timeout: number
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new HttpError(
`Request timeout after ${timeout}ms`,
408,
'Request Timeout',
null,
'TIMEOUT_ERROR'
);
}
throw error;
}
}
/**
* 处理业务逻辑
* @param response 响应对象
* @returns 处理后的响应
*/
private handleBusinessLogic<T = any>(response: HttpResponse<T>): HttpResponse<T> {
const { data, status } = response;
// 检查是否为业务响应结构 { code, msg, data }
if (this.isBusinessResponse(data)) {
const { code, msg, data: businessData } = data;
// 检查业务状态码
if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {
// 业务错误
throw new HttpError(
msg || `Business Error: ${code}`,
status,
response.statusText,
data,
`BUSINESS_ERROR_${code}`
);
}
// 业务成功返回data字段
return {
...response,
data: businessData as T
};
}
return response;
}
/**
* 检查是否为业务响应结构
* @param data 响应数据
* @returns boolean 是否为业务响应结构
*/
private isBusinessResponse(data: any): data is { code: any; msg: string; data: any } {
return (
typeof data === 'object' &&
data !== null &&
('code' in data) &&
('msg' in data) &&
('data' in data)
);
}
/**
* 执行请求拦截器
* @param options 请求选项
* @returns 处理后的请求选项
*/
private async executeRequestInterceptors(options: HttpRequestOptions): Promise<HttpRequestOptions> {
let processedOptions = options;
for (const interceptor of this.requestInterceptors) {
try {
if (interceptor.onRequest) {
processedOptions = await interceptor.onRequest(processedOptions);
}
} catch (error) {
if (interceptor.onRequestError) {
interceptor.onRequestError(error);
}
throw error;
}
}
return processedOptions;
}
/**
* 执行响应拦截器
* @param response 响应对象
* @returns 处理后的响应对象
*/
private async executeResponseInterceptors<T = any>(response: HttpResponse<T>): Promise<HttpResponse<T>> {
let processedResponse = response;
for (const interceptor of this.responseInterceptors) {
try {
if (interceptor.onResponse) {
processedResponse = await interceptor.onResponse(processedResponse);
}
} catch (error) {
if (interceptor.onResponseError) {
return interceptor.onResponseError(error as HttpError);
}
throw error;
}
}
return processedResponse;
}
/**
* 执行响应错误拦截器
* @param error 错误对象
* @returns Promise<never>
*/
private executeResponseErrorInterceptors(error: any): never {
let processedError = error;
for (const interceptor of this.responseInterceptors) {
if (interceptor.onResponseError) {
try {
processedError = interceptor.onResponseError(processedError);
} catch (interceptorError) {
processedError = interceptorError;
}
}
}
if (processedError instanceof HttpError) {
throw processedError;
}
// 转换为HttpError
throw new HttpError(
processedError instanceof Error ? processedError.message : 'Unknown Error',
0,
'Unknown Error',
null,
'UNKNOWN_ERROR'
);
}
/**
* HEAD请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async head<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'HEAD',
url,
...options
});
}
/**
* OPTIONS请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async options<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'OPTIONS',
url,
...options
});
}
/**
* GET请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async get<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'GET',
url,
...options
});
}
/**
* POST请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async post<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'POST',
url,
body,
...options
});
}
/**
* PUT请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async put<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'PUT',
url,
body,
...options
});
}
/**
* DELETE请求
* @param url 请求URL
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async delete<T = any>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'url'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'DELETE',
url,
...options
});
}
/**
* PATCH请求
* @param url 请求URL
* @param body 请求体
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async patch<T = any>(url: string, body?: any, options?: Omit<HttpRequestOptions, 'method' | 'url' | 'body'>): Promise<HttpResponse<T>> {
return this.request<T>({
method: 'PATCH',
url,
body,
...options
});
}
/**
* 创建带基础URL的客户端实例
* @param baseURL 基础URL
* @param config 其他配置
* @returns HttpClient 新的客户端实例
*/
static create(baseURL: string, config?: Omit<HttpClientConfig, 'baseURL'>): HttpClient {
return new HttpClient({
baseURL,
...config
});
}
/**
* 获取当前配置
* @returns HttpClientConfig 当前配置
*/
getConfig(): HttpClientConfig {
return { ...this.config };
}
/**
* 更新配置
* @param config 新的配置
*/
updateConfig(config: Partial<HttpClientConfig>): void {
this.config = {
...this.config,
...config
};
}
/**
* 解析响应体
* @param response 响应对象
* @returns Promise<any> 解析后的响应体
*/
private async parseResponse(response: Response): Promise<any> {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
} else if (contentType.includes('text/')) {
return response.text();
} else {
return response.blob();
}
}
/**
* 解析响应头
* @param headers 响应头对象
* @returns Record<string, string> 解析后的响应头
*/
private parseHeaders(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Token管理模块
* 负责Token的存储、获取、刷新和过期处理
*/
import { Storage } from '../utils/storage';
/**
* Token存储键名常量
*/
const TOKEN_KEY_PREFIX = 'token_';
/**
* Token管理类
* 提供Token的增删改查功能
*/
export class TokenManager {
private readonly storage: Storage;
private readonly tokenKey: string;
private readonly fullTokenKey: string;
/**
* 构造函数
* @param storage 存储实例
* @param clientId 客户端ID用于生成唯一的Token键名
*/
constructor(storage: Storage, clientId: string) {
if (!storage) {
throw new Error('Storage instance is required');
}
if (!clientId || typeof clientId !== 'string') {
throw new Error('Client ID must be a non-empty string');
}
this.storage = storage;
this.tokenKey = clientId;
this.fullTokenKey = `${TOKEN_KEY_PREFIX}${clientId}`;
}
/**
* 存储Token信息
* @param token Token字符串
* @throws {Error} 当token为空或非字符串时抛出错误
*/
saveToken(token: string): void {
if (!token) {
throw new Error('Token must be a non-empty string');
}
this.storage.set(this.fullTokenKey, token);
}
/**
* 获取Token信息
* @returns string | null 返回Token字符串或null
*/
getToken(): string | null {
return this.storage.get(this.fullTokenKey);
}
/**
* 清除Token信息
*/
clearToken(): void {
this.storage.remove(this.fullTokenKey);
}
/**
* 检查Token是否存在
* @returns boolean Token是否存在
*/
hasToken(): boolean {
return this.getToken() !== null;
}
/**
* 获取客户端ID
* @returns string 客户端ID
*/
getClientId(): string {
return this.tokenKey;
}
/**
* 获取完整的Token键名
* @returns string 完整的Token键名
*/
getFullTokenKey(): string {
return this.fullTokenKey;
}
}

View File

@@ -0,0 +1,281 @@
/**
* 路由守卫使用示例
*/
import { RouterGuard, RouterGuardOptions } from './router';
import { Auth } from '../core/auth';
import { Storage } from '../utils/storage';
// 基础使用示例
export function basicUsageExample() {
// 创建存储实例
const storage = new Storage('localStorage');
// 创建认证实例
const auth = new Auth(storage);
// 初始化认证配置
auth.init({
clientId: 'your-client-id',
basepath: 'https://api.example.com',
homePage: '/dashboard',
idpLogoutUrl: 'https://idp.example.com/logout'
} as any);
// 创建路由守卫
const routerGuard = new RouterGuard(auth);
return routerGuard;
}
// Vue路由配置示例
export function vueRouterConfigExample() {
// 在Vue项目中的路由配置
const routes = [
{
path: '/',
name: 'Home',
component: 'HomeView', // 示例组件名
meta: {
auth: {
requiresAuth: false // 首页不需要认证
}
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: 'DashboardView', // 示例组件名
meta: {
auth: {
requiresAuth: true, // 需要登录
redirectUri: '/login' // 登录页面
}
}
},
{
path: '/admin',
name: 'Admin',
component: 'AdminView', // 示例组件名
meta: {
auth: {
requiresAuth: true,
requiredPermissions: ['admin:access'], // 需要管理员权限
unauthorizedRedirectUri: '/unauthorized' // 权限不足跳转页面
}
}
},
{
path: '/user-management',
name: 'UserManagement',
component: 'UserManagementView', // 示例组件名
meta: {
auth: {
requiresAuth: true,
requiredPermissions: ['user:read', 'user:write'], // 需要多个权限
requireAllPermissions: true, // 需要所有权限
requiredRoles: ['admin'], // 需要管理员角色
requireAllRoles: true // 需要所有角色
}
}
},
{
path: '/profile',
name: 'Profile',
component: 'ProfileView', // 示例组件名
meta: {
auth: {
requiresAuth: true,
requiredPermissions: ['profile:view', 'profile:edit'], // 多个权限
requireAllPermissions: false // 任一权限即可
}
}
}
];
return routes;
}
// Vue路由守卫集成示例
export function vueRouterIntegrationExample() {
// 创建路由守卫实例
const storage = new Storage('localStorage');
const auth = new Auth(storage);
const routerGuard = new RouterGuard(auth);
// Vue Router 3.x 配置
const routerConfigV3 = {
routes: vueRouterConfigExample(),
beforeEach: routerGuard.createVueGuard()
};
// Vue Router 4.x 配置
const routerConfigV4 = {
routes: vueRouterConfigExample(),
beforeEnter: routerGuard.createVueGuard()
};
return { routerConfigV3, routerConfigV4 };
}
// 手动权限检查示例
export async function manualPermissionCheckExample(routerGuard: RouterGuard) {
try {
// 检查单个权限
const hasReadPermission = await routerGuard.hasPermission('user:read');
console.log('Has read permission:', hasReadPermission);
// 检查多个权限(需要全部)
const hasAllPermissions = await routerGuard.hasPermission(
['user:read', 'user:write'],
true
);
console.log('Has all permissions:', hasAllPermissions);
// 检查多个权限(需要任一)
const hasAnyPermission = await routerGuard.hasPermission(
['user:read', 'admin:access'],
false
);
console.log('Has any permission:', hasAnyPermission);
// 检查角色
const hasAdminRole = await routerGuard.hasRole('admin');
console.log('Has admin role:', hasAdminRole);
// 获取当前用户
const currentUser = routerGuard.getCurrentUser();
console.log('Current user:', currentUser);
} catch (error) {
console.error('Permission check failed:', error);
}
}
// 高级配置示例
export function advancedConfigurationExample() {
const storage = new Storage('localStorage');
const auth = new Auth(storage);
const routerGuard = new RouterGuard(auth);
// 自定义路由守卫选项
const customGuardOptions: RouterGuardOptions = {
requiresAuth: true,
requiredPermissions: ['custom:permission'],
requiredRoles: ['custom:role'],
requireAllPermissions: true,
requireAllRoles: false,
redirectUri: '/custom-login',
unauthorizedRedirectUri: '/custom-forbidden'
};
return { routerGuard, customGuardOptions };
}
// 错误处理示例
export async function errorHandlingExample(routerGuard: RouterGuard) {
try {
// 执行权限检查
const result = await routerGuard.check({
requiresAuth: true,
requiredPermissions: ['some:permission']
});
if (!result.allowed) {
switch (result.reason) {
case 'NOT_AUTHENTICATED':
console.log('用户未认证,将跳转到登录页');
break;
case 'INSUFFICIENT_PERMISSIONS':
console.log('权限不足:', result.details);
break;
case 'INSUFFICIENT_ROLES':
console.log('角色不足:', result.details);
break;
default:
console.log('访问被拒绝');
}
}
} catch (error) {
console.error('路由守卫检查出错:', error);
}
}
// 实际应用示例 - 完整的Vue组件集成
export class SecureRouteManager {
private routerGuard: RouterGuard;
private auth: Auth;
constructor() {
const storage = new Storage('localStorage');
this.auth = new Auth(storage);
this.routerGuard = new RouterGuard(this.auth);
}
// 初始化SDK
initializeSDK(config: any) {
this.auth.init(config);
}
// 获取Vue路由守卫
getVueRouterGuard() {
return this.routerGuard.createVueGuard();
}
// 手动检查路由权限
async checkRouteAccess(routeMeta: any) {
const options: RouterGuardOptions = routeMeta?.auth || {};
return await this.routerGuard.check(options);
}
// 检查特定权限
async checkSpecificPermission(permission: string) {
return await this.routerGuard.hasPermission(permission);
}
// 检查特定角色
async checkSpecificRole(role: string) {
return await this.routerGuard.hasRole(role);
}
// 获取当前用户信息
getCurrentUser() {
return this.routerGuard.getCurrentUser();
}
// 处理认证状态变化
setupAuthListeners() {
// 监听登录事件
this.auth.on('login', () => {
console.log('用户已登录');
// 可以在这里刷新路由权限等
});
// 监听登出事件
this.auth.on('logout', () => {
console.log('用户已登出');
// 可以在这里清理相关状态
});
}
}
// 使用示例
export function usageDemo() {
// 1. 基础使用
const routerGuard = basicUsageExample();
// 2. 手动权限检查
manualPermissionCheckExample(routerGuard);
// 3. 获取路由配置
const routes = vueRouterConfigExample();
console.log('路由配置:', routes);
// 4. 创建安全路由管理器
const secureManager = new SecureRouteManager();
// 5. 设置监听器
secureManager.setupAuthListeners();
return { routerGuard, secureManager };
}

View File

@@ -0,0 +1,358 @@
/**
* 路由守卫测试文件
*/
import { RouterGuard, RouterGuardOptions } from './router';
import { Auth } from '../core/auth';
import { Storage } from '../utils/storage';
// Mock Auth类
class MockAuth {
private _isAuthenticated = false;
private _userInfo: any = null;
isAuthenticated(): boolean {
return this._isAuthenticated;
}
setAuthenticated(authenticated: boolean): void {
this._isAuthenticated = authenticated;
}
getUserInfo(): any {
return this._userInfo;
}
setUserInfo(userInfo: any): void {
this._userInfo = userInfo;
}
async login(redirectUri?: string): Promise<void> {
// Mock login implementation
console.log('Mock login called with redirectUri:', redirectUri);
}
async hasPermission(permission: string | string[]): Promise<boolean> {
if (!this._isAuthenticated || !this._userInfo) return false;
const permissions = Array.isArray(permission) ? permission : [permission];
const userPermissions = this._userInfo.permissions || [];
return permissions.some(p => userPermissions.includes(p));
}
async hasAllPermissions(permissions: string[]): Promise<boolean> {
if (!this._isAuthenticated || !this._userInfo) return false;
const userPermissions = this._userInfo.permissions || [];
return permissions.every(p => userPermissions.includes(p));
}
async hasRole(role: string | string[]): Promise<boolean> {
if (!this._isAuthenticated || !this._userInfo) return false;
const roles = Array.isArray(role) ? role : [role];
const userRoles = this._userInfo.roles || [];
return roles.some(r => userRoles.includes(r));
}
async hasAllRoles(roles: string[]): Promise<boolean> {
if (!this._isAuthenticated || !this._userInfo) return false;
const userRoles = this._userInfo.roles || [];
return roles.every(r => userRoles.includes(r));
}
}
describe('RouterGuard', () => {
let routerGuard: RouterGuard;
let mockAuth: MockAuth;
let storage: Storage;
beforeEach(() => {
storage = new Storage('localStorage');
mockAuth = new MockAuth() as any;
routerGuard = new RouterGuard(mockAuth as unknown as Auth);
});
describe('构造函数', () => {
test('应该正确初始化', () => {
expect(routerGuard).toBeInstanceOf(RouterGuard);
});
test('应该在没有Auth实例时抛出错误', () => {
expect(() => new RouterGuard(null as any)).toThrow('Auth instance is required');
});
});
describe('权限检查', () => {
test('应该允许不需要认证的路由', async () => {
const options: RouterGuardOptions = {
requiresAuth: false
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
test('应该拒绝未认证用户的访问', async () => {
const options: RouterGuardOptions = {
requiresAuth: true
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(false);
expect(result.reason).toBe('NOT_AUTHENTICATED');
});
test('应该允许已认证用户的访问', async () => {
// 设置用户为已认证状态
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['user']
});
const options: RouterGuardOptions = {
requiresAuth: true
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
});
test('应该检查单个权限', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user', 'write:user'],
roles: ['user']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredPermissions: ['read:user']
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
});
test('应该拒绝缺少权限的访问', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['user']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredPermissions: ['write:user']
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(false);
expect(result.reason).toBe('INSUFFICIENT_PERMISSIONS');
});
test('应该检查多个权限(需要全部)', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user', 'write:user', 'delete:user'],
roles: ['admin']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredPermissions: ['read:user', 'write:user'],
requireAllPermissions: true
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
});
test('应该检查多个权限(需要任一)', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['user']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredPermissions: ['read:user', 'write:user'],
requireAllPermissions: false
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
});
test('应该检查角色', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['admin', 'user']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredRoles: ['admin']
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(true);
});
test('应该拒绝缺少角色的访问', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['user']
});
const options: RouterGuardOptions = {
requiresAuth: true,
requiredRoles: ['admin']
};
const result = await routerGuard.check(options);
expect(result.allowed).toBe(false);
expect(result.reason).toBe('INSUFFICIENT_ROLES');
});
});
describe('hasPermission方法', () => {
beforeEach(() => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user', 'write:user'],
roles: ['user']
});
});
test('应该检查单个权限', async () => {
const result = await routerGuard.hasPermission('read:user');
expect(result).toBe(true);
});
test('应该检查多个权限(需要全部)', async () => {
const result = await routerGuard.hasPermission(['read:user', 'write:user'], true);
expect(result).toBe(true);
});
test('应该检查多个权限(需要任一)', async () => {
const result = await routerGuard.hasPermission(['read:user', 'delete:user'], false);
expect(result).toBe(true);
});
test('应该在未认证时返回false', async () => {
mockAuth.setAuthenticated(false);
const result = await routerGuard.hasPermission('read:user');
expect(result).toBe(false);
});
test('应该在空权限时返回true', async () => {
const result = await routerGuard.hasPermission([]);
expect(result).toBe(true);
});
test('应该在无权限参数时返回true', async () => {
const result = await routerGuard.hasPermission(null as any);
expect(result).toBe(true);
});
});
describe('hasRole方法', () => {
beforeEach(() => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['admin', 'user']
});
});
test('应该检查单个角色', async () => {
const result = await routerGuard.hasRole('admin');
expect(result).toBe(true);
});
test('应该检查多个角色(需要全部)', async () => {
const result = await routerGuard.hasRole(['admin', 'user'], true);
expect(result).toBe(true);
});
test('应该检查多个角色(需要任一)', async () => {
const result = await routerGuard.hasRole(['admin', 'superuser'], false);
expect(result).toBe(true);
});
test('应该在未认证时返回false', async () => {
mockAuth.setAuthenticated(false);
const result = await routerGuard.hasRole('admin');
expect(result).toBe(false);
});
});
describe('getCurrentUser方法', () => {
test('应该返回当前用户信息', () => {
const userInfo = { userId: '1', username: 'test' };
mockAuth.setUserInfo(userInfo);
const result = routerGuard.getCurrentUser();
expect(result).toEqual(userInfo);
});
test('应该在获取用户信息失败时返回null', () => {
// Mock getUserInfo 抛出异常
jest.spyOn(mockAuth, 'getUserInfo').mockImplementation(() => {
throw new Error('Failed to get user info');
});
const result = routerGuard.getCurrentUser();
expect(result).toBeNull();
});
});
describe('Vue路由守卫', () => {
test('应该创建有效的路由守卫函数', () => {
const guard = routerGuard.createVueGuard();
expect(typeof guard).toBe('function');
});
test('应该正确处理允许的路由', async () => {
mockAuth.setAuthenticated(true);
mockAuth.setUserInfo({
permissions: ['read:user'],
roles: ['user']
});
const guard = routerGuard.createVueGuard();
const to = { meta: { auth: { requiresAuth: true } } };
const from = {};
const next = jest.fn();
await guard(to as any, from as any, next);
expect(next).toHaveBeenCalledWith();
});
test('应该正确处理拒绝的路由', async () => {
const guard = routerGuard.createVueGuard();
const to = { meta: { auth: { requiresAuth: true } } };
const from = {};
const next = jest.fn();
await guard(to as any, from as any, next);
expect(next).toHaveBeenCalledWith(false);
});
test('应该处理路由守卫错误', async () => {
// Mock check 方法抛出异常
jest.spyOn(routerGuard, 'check').mockRejectedValue(new Error('Test error'));
const guard = routerGuard.createVueGuard();
const to = { meta: { auth: {} } };
const from = {};
const next = jest.fn();
await guard(to as any, from as any, next);
expect(next).toHaveBeenCalledWith(false);
});
});
});

View File

@@ -0,0 +1,311 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
import { Auth } from '../core/auth';
import { UserInfo } from '../types';
/**
* 路由守卫选项
*/
export interface RouterGuardOptions {
/**
* 是否需要登录
* @default true
*/
requiresAuth?: boolean;
/**
* 需要的权限列表
*/
requiredPermissions?: string[];
/**
* 需要的角色列表
*/
requiredRoles?: string[];
/**
* 登录后重定向的URL
*/
redirectUri?: string;
/**
* 权限不足时重定向的URL
*/
unauthorizedRedirectUri?: string;
/**
* 是否需要所有权限true还是任一权限false
* @default true
*/
requireAllPermissions?: boolean;
/**
* 是否需要所有角色true还是任一角色false
* @default true
*/
requireAllRoles?: boolean;
}
/**
* 路由守卫检查结果
*/
export interface GuardCheckResult {
/** 是否允许访问 */
allowed: boolean;
/** 拒绝原因 */
reason?: 'NOT_AUTHENTICATED' | 'INSUFFICIENT_PERMISSIONS' | 'INSUFFICIENT_ROLES';
/** 详细信息 */
details?: string;
}
/**
* Vue路由守卫函数类型
*/
type VueRouteGuard = (to: any, from: any, next: any) => Promise<void>;
/**
* 路由守卫类
*/
export class RouterGuard {
private readonly auth: Auth;
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth: Auth) {
if (!auth) {
throw new Error('Auth instance is required');
}
this.auth = auth;
}
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<GuardCheckResult> 权限检查结果
*/
async check(options: RouterGuardOptions): Promise<GuardCheckResult> {
const {
requiresAuth = true,
requiredPermissions = [],
requiredRoles = [],
requireAllPermissions = true,
requireAllRoles = true
} = options;
// 如果不需要认证,直接允许
if (!requiresAuth) {
return { allowed: true };
}
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
// 未认证,触发登录流程
try {
await this.auth.login(options.redirectUri);
} catch (error) {
console.error('Failed to initiate login:', error);
}
return {
allowed: false,
reason: 'NOT_AUTHENTICATED',
details: 'User is not authenticated'
};
}
// 检查权限
if (requiredPermissions.length > 0) {
const hasPermission = await this.checkPermissions(
requiredPermissions,
requireAllPermissions
);
if (!hasPermission) {
this.handleUnauthorizedAccess(options.unauthorizedRedirectUri);
return {
allowed: false,
reason: 'INSUFFICIENT_PERMISSIONS',
details: `Missing required permissions: ${requiredPermissions.join(', ')}`
};
}
}
// 检查角色
if (requiredRoles.length > 0) {
const hasRole = await this.checkRoles(requiredRoles, requireAllRoles);
if (!hasRole) {
this.handleUnauthorizedAccess(options.unauthorizedRedirectUri);
return {
allowed: false,
reason: 'INSUFFICIENT_ROLES',
details: `Missing required roles: ${requiredRoles.join(', ')}`
};
}
}
return { allowed: true };
}
/**
* 创建Vue路由守卫
* @returns Vue路由守卫函数
*/
createVueGuard(): VueRouteGuard {
return async (to: any, from: any, next: any) => {
try {
// 从路由元信息中获取守卫选项
const options: RouterGuardOptions = to.meta?.auth || {};
const result = await this.check(options);
if (result.allowed) {
next();
} else {
// 根据拒绝原因决定如何处理
switch (result.reason) {
case 'NOT_AUTHENTICATED':
// 登录已经在check方法中处理
next(false);
break;
case 'INSUFFICIENT_PERMISSIONS':
case 'INSUFFICIENT_ROLES':
// 权限不足已经在check方法中处理
next(false);
break;
default:
next(false);
}
}
} catch (error) {
console.error('Route guard error:', error);
next(false);
}
};
}
/**
* 检查当前用户是否有权限访问资源
* @param permissions 需要的权限列表
* @param requireAll 是否需要所有权限
* @returns Promise<boolean> 是否拥有权限
*/
async hasPermission(permissions: string | string[], requireAll: boolean = true): Promise<boolean> {
if (!permissions) {
return true;
}
const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];
if (requiredPermissions.length === 0) {
return true;
}
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
return false;
}
// 使用Auth类的内置权限检查方法
try {
if (requireAll) {
return await this.auth.hasAllPermissions(requiredPermissions);
} else {
return await this.auth.hasPermission(requiredPermissions);
}
} catch (error) {
console.error('Permission check failed:', error);
return false;
}
}
/**
* 检查当前用户是否有指定角色
* @param roles 需要的角色列表
* @param requireAll 是否需要所有角色
* @returns Promise<boolean> 是否拥有角色
*/
async hasRole(roles: string | string[], requireAll: boolean = true): Promise<boolean> {
if (!roles) {
return true;
}
const requiredRoles = Array.isArray(roles) ? roles : [roles];
if (requiredRoles.length === 0) {
return true;
}
// 检查是否已认证
if (!this.auth.isAuthenticated()) {
return false;
}
// 使用Auth类的内置角色检查方法
try {
if (requireAll) {
return await this.auth.hasAllRoles(requiredRoles);
} else {
return await this.auth.hasRole(requiredRoles);
}
} catch (error) {
console.error('Role check failed:', error);
return false;
}
}
/**
* 获取当前用户信息
* @returns UserInfo | null 用户信息
*/
async getCurrentUser(): Promise<UserInfo | null> {
try {
return await this.auth.getUserInfo();
} catch (error) {
console.error('Failed to get user info:', error);
return null;
}
}
/**
* 检查权限
* @private
*/
private async checkPermissions(permissions: string[], requireAll: boolean): Promise<boolean> {
try {
return requireAll
? await this.auth.hasAllPermissions(permissions)
: await this.auth.hasPermission(permissions);
} catch (error) {
console.error('Permission validation failed:', error);
return false;
}
}
/**
* 检查角色
* @private
*/
private async checkRoles(roles: string[], requireAll: boolean): Promise<boolean> {
try {
return requireAll
? await this.auth.hasAllRoles(roles)
: await this.auth.hasRole(roles);
} catch (error) {
console.error('Role validation failed:', error);
return false;
}
}
/**
* 处理未授权访问
* @private
*/
private handleUnauthorizedAccess(redirectUri?: string): void {
if (redirectUri) {
try {
window.location.href = redirectUri;
} catch (error) {
console.error('Failed to redirect to unauthorized page:', error);
}
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* 统一登录SDK入口文件
* 支持OAuth2授权码模式提供完整的认证和权限管理功能
*/
// 核心功能导出
export { Auth } from './core/auth';
export { TokenManager } from './core/token';
export { HttpClient, HttpError } from './core/http';
// 工具类导出
export { Storage } from './utils/storage';
export { RouterGuard, RouterGuardOptions } from './guards/router';
// Vue集成导出
export { VuePlugin, createVuePlugin, useAuth } from './plugins/vue';
// 工具函数导出
export {
generateRandomString,
parseQueryParams,
buildQueryParams,
generateAuthorizationUrl,
isCallbackUrl
} from './utils/url';
// 类型定义导出
export type {
SDKConfig,
UnifiedLoginSDK,
UserInfo,
RouterInfo,
EventType
} from './types';
// 创建默认SDK实例
import { SDKConfig, UnifiedLoginSDK } from './types';
import { Auth } from './core/auth';
import { Storage } from './utils/storage';
// 全局单例实例
let defaultSDK: UnifiedLoginSDK | null = null;
let defaultAuth: Auth | null = null;
/**
* 获取默认SDK实例
* @returns UnifiedLoginSDK SDK实例
*/
function getDefaultSDK(): UnifiedLoginSDK {
if (!defaultSDK) {
const storage = new Storage();
defaultAuth = new Auth(storage);
defaultSDK = {
init: (config: SDKConfig) => defaultAuth!.init(config),
getToken: () => defaultAuth!.getToken(),
login: (redirectUri?: string) => defaultAuth!.login(redirectUri),
logout: () => defaultAuth!.logout(),
handleCallback: () => defaultAuth!.handleCallback(),
getRoutes: () => defaultAuth!.getRoutes(),
getUserInfo: () => defaultAuth!.getUserInfo(),
refreshUserInfo: () => defaultAuth!.refreshUserInfo(),
isAuthenticated: () => defaultAuth!.isAuthenticated(),
hasRole: (role: string | string[]) => defaultAuth!.hasRole(role),
hasAllRoles: (roles: string[]) => defaultAuth!.hasAllRoles(roles),
hasPermission: (permission: string | string[]) => defaultAuth!.hasPermission(permission),
hasAllPermissions: (permissions: string[]) => defaultAuth!.hasAllPermissions(permissions),
on: (event, callback) => defaultAuth!.on(event, callback),
off: (event, callback) => defaultAuth!.off(event, callback),
isCallback: () => defaultAuth!.isCallback()
};
}
return defaultSDK!;
}
/**
* 默认导出的SDK实例
* 提供最简化的使用方式
*/
export const unifiedLoginSDK = getDefaultSDK();
/**
* 默认导出
*/
export default unifiedLoginSDK;
// 版本信息
export const VERSION = '1.0.0';
export const version = VERSION;
// 便捷别名导出
export {
unifiedLoginSDK as sdk,
unifiedLoginSDK as auth
};
// 全局类型声明
declare global {
interface Window {
unifiedLoginSDK: typeof unifiedLoginSDK;
}
}
// 如果在浏览器环境中将SDK挂载到window对象
if (typeof window !== 'undefined') {
window.unifiedLoginSDK = unifiedLoginSDK;
}

View File

@@ -0,0 +1,260 @@
/**
* Vue插件测试文件
*/
import { VuePlugin, VuePluginOptions, createVuePlugin, useAuth } from './vue';
import { Storage } from '../utils/storage';
// Mock Storage类
class MockStorage {
private data: Record<string, any> = {};
set(key: string, value: any): void {
this.data[key] = value;
}
get(key: string): any {
return this.data[key];
}
remove(key: string): void {
delete this.data[key];
}
}
// Mock Auth类
class MockAuth {
init(config: any): void {
console.log('Auth initialized with config:', config);
}
login(): void {
console.log('Login called');
}
logout(): void {
console.log('Logout called');
}
}
// Mock RouterGuard类
class MockRouterGuard {
createVueGuard() {
return (to: any, from: any, next: any) => {
console.log('Route guard called');
next();
};
}
}
// Mock Vue 3 App
class MockVue3App {
config = {
globalProperties: {} as Record<string, any>
};
provide = jest.fn();
mixin = jest.fn();
use = jest.fn();
}
// Mock Vue 2 Constructor
class MockVue2Constructor {
static prototype: Record<string, any> = {};
static mixin = jest.fn();
static use = jest.fn();
}
describe('VuePlugin', () => {
let storage: Storage;
let vuePlugin: VuePlugin;
beforeEach(() => {
storage = new MockStorage() as any;
vuePlugin = new VuePlugin(storage);
});
describe('构造函数', () => {
test('应该正确初始化', () => {
expect(vuePlugin).toBeInstanceOf(VuePlugin);
expect(vuePlugin.getAuth()).toBeDefined();
expect(vuePlugin.getRouterGuard()).toBeDefined();
expect(vuePlugin.getStorage()).toBe(storage);
});
test('应该在没有存储实例时抛出错误', () => {
expect(() => new VuePlugin(null as any)).toThrow('Storage instance is required');
});
});
describe('install方法', () => {
test('应该在没有配置时抛出错误', () => {
const app = new MockVue3App();
const options: VuePluginOptions = { config: undefined as any };
expect(() => vuePlugin.install(app as any, options)).toThrow('SDK config is required');
});
test('应该正确安装Vue 3插件', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any,
pluginName: 'testLogin'
};
vuePlugin.install(app as any, options);
// 检查全局属性
expect(app.config.globalProperties.$testLogin).toBeDefined();
expect(app.config.globalProperties.$auth).toBeDefined();
// 检查provide调用
expect(app.provide).toHaveBeenCalledWith('testLogin', expect.anything());
expect(app.provide).toHaveBeenCalledWith('auth', expect.anything());
});
test('应该正确安装Vue 2插件', () => {
const app = MockVue2Constructor;
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any,
pluginName: 'testLogin'
};
vuePlugin.install(app as any, options);
// 检查原型属性
expect(app.prototype.$testLogin).toBeDefined();
expect(app.prototype.$auth).toBeDefined();
});
test('应该防止重复初始化', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any
};
// 第一次安装
vuePlugin.install(app as any, options);
// 第二次安装应该输出警告
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
vuePlugin.install(app as any, options);
expect(consoleWarnSpy).toHaveBeenCalledWith('Vue plugin is already initialized');
consoleWarnSpy.mockRestore();
});
test('应该支持手动指定Vue版本', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any,
vueVersion: 'vue2' // 强制使用Vue 2模式
};
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
vuePlugin.install(app as any, options);
expect(consoleLogSpy).toHaveBeenCalledWith('Unified Login SDK installed successfully (vue2)');
consoleLogSpy.mockRestore();
});
});
describe('getter方法', () => {
test('应该正确返回各个实例', () => {
expect(vuePlugin.getAuth()).toBeDefined();
expect(vuePlugin.getRouterGuard()).toBeDefined();
expect(vuePlugin.getStorage()).toBe(storage);
expect(vuePlugin.getSDK()).toBeDefined();
});
});
describe('辅助函数', () => {
test('createVuePlugin应该创建插件实例', () => {
const plugin = createVuePlugin('localStorage', 'test_prefix');
expect(plugin).toBeInstanceOf(VuePlugin);
expect(plugin.getStorage()).toBeInstanceOf(Storage);
});
test('useAuth应该返回正确的composable对象', () => {
const composable = useAuth(vuePlugin);
expect(composable.auth).toBe(vuePlugin.getAuth());
expect(composable.routerGuard).toBe(vuePlugin.getRouterGuard());
expect(composable.storage).toBe(vuePlugin.getStorage());
expect(composable.sdk).toBe(vuePlugin.getSDK());
});
});
describe('路由守卫注册', () => {
test('应该在启用时注册路由守卫', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any,
autoRegisterGuards: true
};
vuePlugin.install(app as any, options);
expect(app.mixin).toHaveBeenCalled();
});
test('应该在禁用时不注册路由守卫', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any,
autoRegisterGuards: false
};
vuePlugin.install(app as any, options);
expect(app.mixin).not.toHaveBeenCalled();
});
});
describe('Vue版本检测', () => {
test('应该正确检测Vue 3', () => {
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any
};
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
vuePlugin.install(app as any, options);
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('(vue3)'));
consoleLogSpy.mockRestore();
});
test('应该正确检测Vue 2', () => {
const app = MockVue2Constructor;
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any
};
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
vuePlugin.install(app as any, options);
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('(vue2)'));
consoleLogSpy.mockRestore();
});
});
});
describe('错误处理', () => {
test('应该正确处理安装过程中的错误', () => {
const storage = new MockStorage() as any;
const plugin = new VuePlugin(storage);
// Mock auth.init 抛出错误
const mockAuth = plugin.getAuth();
jest.spyOn(mockAuth, 'init').mockImplementation(() => {
throw new Error('Init failed');
});
const app = new MockVue3App();
const options: VuePluginOptions = {
config: { clientId: 'test-client' } as any
};
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
expect(() => plugin.install(app as any, options)).toThrow('Init failed');
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to install Vue plugin:', expect.any(Error));
consoleErrorSpy.mockRestore();
});
});

View File

@@ -0,0 +1,185 @@
/**
* Vue插件模块
* 提供Vue应用中使用统一登录SDK的能力
*/
import { Auth } from '../core/auth';
import { SDKConfig, UnifiedLoginSDK } from '../types';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue版本类型
*/
type VueVersion = 'vue2' | 'vue3';
/**
* Vue插件选项
*/
export interface VuePluginOptions {
/**
* SDK配置
*/
config: SDKConfig;
/**
* 插件名称,默认'unifiedLogin'
*/
pluginName?: string;
}
/**
* Vue插件类
*/
export class VuePlugin {
private readonly auth: Auth;
private readonly routerGuard: RouterGuard;
private readonly storage: Storage;
/**
* 构造函数
* @param storage 存储实例
*/
constructor(storage: Storage) {
if (!storage) {
throw new Error('Storage instance is required');
}
this.storage = storage;
this.auth = new Auth(storage);
this.routerGuard = new RouterGuard(this.auth);
}
/**
* 安装Vue插件
* @param app Vue构造函数或Vue 3应用实例
* @param options 插件选项
*/
install(app: any, options: VuePluginOptions): void {
const { config, pluginName = 'unifiedLogin' } = options;
// 初始化SDK
this.auth.init(config);
// 判断是Vue 2还是Vue 3
const isVue3 = typeof app.config !== 'undefined';
if (isVue3) {
// Vue 3
// 在全局属性上挂载SDK实例
app.config.globalProperties[`${pluginName}`] = this.auth;
app.config.globalProperties.$auth = this.auth; // 兼容简写
// 提供Vue组件内的注入
app.provide(pluginName, this.auth);
app.provide('auth', this.auth); // 兼容简写
// 处理路由守卫
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
} else {
// Vue 2
// 在Vue实例上挂载SDK实例
app.prototype[`${pluginName}`] = this.auth;
app.prototype.$auth = this.auth; // 兼容简写
// 全局混入
app.mixin({
beforeCreate() {
// 如果是根组件,添加路由守卫
if (this.$options.router) {
const router = this.$options.router;
// 添加全局前置守卫
router.beforeEach(this.routerGuard.createVueGuard());
}
}
});
}
}
/**
* 获取认证实例
* @returns Auth 认证实例
*/
getAuth(): Auth {
return this.auth;
}
/**
* 获取路由守卫实例
* @returns RouterGuard 路由守卫实例
*/
getRouterGuard(): RouterGuard {
return this.routerGuard;
}
/**
* 获取存储实例
* @returns Storage 存储实例
*/
getStorage(): Storage {
return this.storage;
}
/**
* 获取SDK实例兼容旧版本
* @returns UnifiedLoginSDK SDK实例
*/
getSDK(): UnifiedLoginSDK {
return this.auth as unknown as UnifiedLoginSDK;
}
}
/**
* 创建Vue插件实例
* @param storageType 存储类型
* @param prefix 存储前缀
* @returns VuePlugin Vue插件实例
*/
export function createVuePlugin(
storageType?: 'localStorage' | 'sessionStorage' | 'cookie',
prefix?: string
): VuePlugin {
const storage = new Storage(storageType, prefix);
return new VuePlugin(storage);
}
/**
* 创建Vue 3 Composition API使用的composable函数
* @param plugin Vue插件实例
* @returns composable函数
*/
export function useAuth(plugin: VuePlugin) {
return {
/** 认证实例 */
auth: plugin.getAuth(),
/** 路由守卫实例 */
routerGuard: plugin.getRouterGuard(),
/** 存储实例 */
storage: plugin.getStorage(),
/** SDK实例 */
sdk: plugin.getSDK()
};
}
// 全局类型声明(可根据需要在项目中添加)
// declare module '@vue/runtime-core' {
// interface ComponentCustomProperties {
// $unifiedLogin: Auth;
// $auth: Auth;
// }
// }
//
// declare module 'vue/types/vue' {
// interface Vue {
// $unifiedLogin: Auth;
// $auth: Auth;
// }
// }

View File

@@ -0,0 +1,22 @@
/**
* SDK配置选项
*/
export interface SDKConfig {
/** 客户端ID */
clientId: string;
/** 注册id **/
registrationId: string,
/** 后端basepath路径*/
basepath: string,
/** 存储类型默认localStorage */
storageType?: 'localStorage' | 'sessionStorage' | 'cookie';
idpLogoutUrl:string;
homePage: string;
/** 租户ID可选 */
tenantId?: string;
}
/**
* 事件类型
*/
export type EventType = 'login' | 'logout' | 'tokenExpired';

View File

@@ -0,0 +1,100 @@
import {RouterInfo} from "./user";
export * from './config';
export * from './user';
/**
* 统一登录SDK接口
*/
export interface UnifiedLoginSDK {
/**
* 初始化SDK配置
* @param config SDK配置选项
*/
init(config: import('./config').SDKConfig): void;
getToken():string|null
/**
* 触发登录流程
* @param redirectUri 可选的重定向URL覆盖初始化时的配置
*/
login(redirectUri?: string): Promise<void>;
/**
* 退出登录
*/
logout(): Promise<void>;
/**
* 处理授权回调
* @returns Promise<UserInfo> 用户信息
*/
handleCallback(): Promise<void>;
getRoutes(): Promise<import('./user').RouterInfo>;
/**
* 获取用户信息
* @returns Promise<UserInfo> 用户信息
*/
getUserInfo(): Promise<import('./user').UserInfo>;
/**
* 刷新用户信息缓存
* @returns Promise<UserInfo> 更新后的用户信息
*/
refreshUserInfo(): Promise<import('./user').UserInfo>;
/**
* 检查用户是否已认证
* @returns boolean 是否已认证
*/
isAuthenticated(): boolean;
/**
* 检查用户是否有指定角色
* @param role 角色编码或角色编码列表
* @returns Promise<boolean> 是否有指定角色
*/
hasRole(role: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定角色
* @param roles 角色编码列表
* @returns Promise<boolean> 是否有所有指定角色
*/
hasAllRoles(roles: string[]): Promise<boolean>;
/**
* 检查用户是否有指定权限
* @param permission 权限标识或权限标识列表
* @returns Promise<boolean> 是否有指定权限
*/
hasPermission(permission: string | string[]): Promise<boolean>;
/**
* 检查用户是否有所有指定权限
* @param permissions 权限标识列表
* @returns Promise<boolean> 是否有所有指定权限
*/
hasAllPermissions(permissions: string[]): Promise<boolean>;
/**
* 事件监听
* @param event 事件类型
* @param callback 回调函数
*/
on(event: import('./config').EventType, callback: Function): void;
/**
* 移除事件监听
* @param event 事件类型
* @param callback 回调函数
*/
off(event: import('./config').EventType, callback: Function): void;
/**
* 检查当前URL是否为授权回调
* @returns boolean 是否为授权回调
*/
isCallback(): boolean;
}

View File

@@ -0,0 +1,88 @@
/**
* 菜单信息
*/
export interface RouterInfo {
/** 菜单名称 */
name: string;
/** 菜单路径 */
path?: string;
hidden: boolean;
redirect: string;
query: string;
alwaysShow: boolean;
/** 菜单组件 */
component?: string;
meta:MetaVo;
children: RouterInfo;
}
export interface MetaVo {
/**
* 设置该路由在侧边栏和面包屑中展示的名字
*/
title:string;
/**
* 设置该路由的图标对应路径src/assets/icons/svg
*/
icon:string;
/**
* 设置为true则不会被 <keep-alive>缓存
*/
noCache:boolean;
/**
* 内链地址http(s)://开头)
*/
link:string;
}
/**
* 用户基本信息
*/
export interface UserInfo {
/** 用户ID */
userId: string;
/** 用户名 */
username: string;
/** 姓名 */
nickName: string;
/** 邮箱 */
currentDeptId: string;
/** 部门 */
userDepts?: UserDept[];
/** 岗位 */
userPost?: UserPost[];
/** 性别 */
sex: string;
/** 用户角色 */
roles?: string[];
/** 权限列表 */
permissions?: string[];
dataPermission: DataPermission
}
export interface DataPermission {
allowAll: boolean;
onlySelf: boolean;
deptList?: string[];
areas?:string[]
}
export interface UserDept{
postCode:string
postId:bigint
postName:string
postSort:bigint
remark:string
status:bigint
}
export interface UserPost{
ancestors: string
deptId: bigint
deptName: string
leader: string
orderNum: bigint
parentId: bigint
status: bigint
}

View File

@@ -0,0 +1,358 @@
/**
* 存储工具类
* 支持localStorage、sessionStorage和cookie三种存储方式
*/
type StorageType = 'localStorage' | 'sessionStorage' | 'cookie';
/**
* 存储工具类
*/
export class Storage {
private storageType: StorageType;
private prefix: string;
/**
* 构造函数
* @param storageType 存储类型
* @param prefix 存储前缀,默认'unified_login_'
*/
constructor(storageType: StorageType = 'localStorage', prefix: string = 'unified_login_') {
this.storageType = storageType;
this.prefix = prefix;
}
/**
* 设置存储项
* @param key 存储键
* @param value 存储值
* @param options 可选参数cookie存储时使用
*/
set(key: string, value: any, options?: { expires?: number; path?: string; domain?: string; secure?: boolean }): void {
const fullKey = this.prefix + key;
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
switch (this.storageType) {
case 'localStorage':
this.setLocalStorage(fullKey, stringValue);
break;
case 'sessionStorage':
this.setSessionStorage(fullKey, stringValue);
break;
case 'cookie':
this.setCookie(fullKey, stringValue, options);
break;
}
}
/**
* 获取存储项
* @param key 存储键
* @returns 存储值
*/
get(key: string): any {
const fullKey = this.prefix + key;
let value: any;
switch (this.storageType) {
case 'localStorage':
value = this.getLocalStorage(fullKey);
break;
case 'sessionStorage':
value = this.getSessionStorage(fullKey);
break;
case 'cookie':
value = this.getCookie(fullKey);
break;
default:
value = null;
}
if (value === null) {
return null;
}
// 尝试解析JSON
try {
return JSON.parse(value);
} catch (e) {
// 如果不是JSON直接返回字符串
return value;
}
}
/**
* 移除存储项
* @param key 存储键
*/
remove(key: string): void {
const fullKey = this.prefix + key;
switch (this.storageType) {
case 'localStorage':
this.removeLocalStorage(fullKey);
break;
case 'sessionStorage':
this.removeSessionStorage(fullKey);
break;
case 'cookie':
this.removeCookie(fullKey);
break;
}
}
/**
* 清空所有存储项
*/
clear(): void {
switch (this.storageType) {
case 'localStorage':
this.clearLocalStorage();
break;
case 'sessionStorage':
this.clearSessionStorage();
break;
case 'cookie':
this.clearCookie();
break;
}
}
/**
* 检查存储类型是否可用
* @returns boolean 是否可用
*/
isAvailable(): boolean {
try {
switch (this.storageType) {
case 'localStorage':
return this.isLocalStorageAvailable();
case 'sessionStorage':
return this.isSessionStorageAvailable();
case 'cookie':
return typeof document !== 'undefined';
default:
return false;
}
} catch (e) {
return false;
}
}
// ------------------------ localStorage 操作 ------------------------
/**
* 设置localStorage
*/
private setLocalStorage(key: string, value: string): void {
if (this.isLocalStorageAvailable()) {
localStorage.setItem(key, value);
}
}
/**
* 获取localStorage
*/
private getLocalStorage(key: string): string | null {
if (this.isLocalStorageAvailable()) {
return localStorage.getItem(key);
}
return null;
}
/**
* 移除localStorage
*/
private removeLocalStorage(key: string): void {
if (this.isLocalStorageAvailable()) {
localStorage.removeItem(key);
}
}
/**
* 清空localStorage中所有带前缀的项
*/
private clearLocalStorage(): void {
if (this.isLocalStorageAvailable()) {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(this.prefix)) {
localStorage.removeItem(key);
i--; // 索引调整
}
}
}
}
/**
* 检查localStorage是否可用
*/
private isLocalStorageAvailable(): boolean {
if (typeof localStorage === 'undefined') {
return false;
}
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// ------------------------ sessionStorage 操作 ------------------------
/**
* 设置sessionStorage
*/
private setSessionStorage(key: string, value: string): void {
if (this.isSessionStorageAvailable()) {
sessionStorage.setItem(key, value);
}
}
/**
* 获取sessionStorage
*/
private getSessionStorage(key: string): string | null {
if (this.isSessionStorageAvailable()) {
return sessionStorage.getItem(key);
}
return null;
}
/**
* 移除sessionStorage
*/
private removeSessionStorage(key: string): void {
if (this.isSessionStorageAvailable()) {
sessionStorage.removeItem(key);
}
}
/**
* 清空sessionStorage中所有带前缀的项
*/
private clearSessionStorage(): void {
if (this.isSessionStorageAvailable()) {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith(this.prefix)) {
sessionStorage.removeItem(key);
i--; // 索引调整
}
}
}
}
/**
* 检查sessionStorage是否可用
*/
private isSessionStorageAvailable(): boolean {
if (typeof sessionStorage === 'undefined') {
return false;
}
try {
const testKey = '__storage_test__';
sessionStorage.setItem(testKey, testKey);
sessionStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// ------------------------ cookie 操作 ------------------------
/**
* 设置cookie
*/
private setCookie(
key: string,
value: string,
options?: { expires?: number; path?: string; domain?: string; secure?: boolean }
): void {
if (typeof document === 'undefined') {
return;
}
let cookieString = `${key}=${encodeURIComponent(value)}`;
if (options) {
// 设置过期时间(秒)
if (options.expires) {
const date = new Date();
date.setTime(date.getTime() + options.expires * 1000);
cookieString += `; expires=${date.toUTCString()}`;
}
// 设置路径
if (options.path) {
cookieString += `; path=${options.path}`;
}
// 设置域名
if (options.domain) {
cookieString += `; domain=${options.domain}`;
}
// 设置secure
if (options.secure) {
cookieString += '; secure';
}
}
document.cookie = cookieString;
}
/**
* 获取cookie
*/
private getCookie(key: string): string | null {
if (typeof document === 'undefined') {
return null;
}
const name = `${key}=`;
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return null;
}
/**
* 移除cookie
*/
private removeCookie(key: string): void {
this.setCookie(key, '', { expires: -1 });
}
/**
* 清空所有带前缀的cookie
*/
private clearCookie(): void {
if (typeof document === 'undefined') {
return;
}
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const eqPos = cookie.indexOf('=');
const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
if (key.startsWith(this.prefix)) {
this.removeCookie(key);
}
}
}
}

View File

@@ -0,0 +1,125 @@
/**
* URL处理工具
* 用于生成授权URL、解析URL参数等功能
*/
/**
* 生成随机字符串
* @param length 字符串长度默认32位
* @returns 随机字符串
*/
export function generateRandomString(length: number = 32): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 解析URL查询参数
* @param url URL字符串默认为当前URL
* @returns 查询参数对象
*/
export function parseQueryParams(url: string = window.location.href): Record<string, string> {
const params: Record<string, string> = {};
const queryString = url.split('?')[1];
if (!queryString) {
return params;
}
const pairs = queryString.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key) {
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
}
}
return params;
}
/**
* 构建URL查询参数
* @param params 查询参数对象
* @returns 查询参数字符串
*/
export function buildQueryParams(params: Record<string, any>): string {
const pairs: string[] = [];
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
}
return pairs.length ? `?${pairs.join('&')}` : '';
}
/**
* 生成OAuth2授权URL
* @param authorizationEndpoint 授权端点URL
* @param clientId 客户端ID
* @param redirectUri 重定向URL
* @param options 可选参数
* @returns 授权URL
*/
export function generateAuthorizationUrl(
authorizationEndpoint: string,
clientId: string,
redirectUri: string,
options?: {
responseType?: string;
scope?: string;
state?: string;
[key: string]: any;
}
): string {
const {
responseType = 'code',
scope,
state = generateRandomString(32),
...extraParams
} = options || {};
const params = {
client_id: clientId,
redirect_uri: redirectUri,
response_type: responseType,
state,
...(scope ? { scope } : {}),
...extraParams
};
const queryString = buildQueryParams(params);
return `${authorizationEndpoint}${queryString}`;
}
/**
* 检查当前URL是否为授权回调
* @param url URL字符串默认为当前URL
* @returns 是否为授权回调
*/
export function isCallbackUrl(url: string = window.location.href): boolean {
const params = parseQueryParams(url);
return !!params.code || !!params.error;
}
/**
* 获取当前URL的路径名
* @param url URL字符串默认为当前URL
* @returns 路径名
*/
export function getPathname(url: string = window.location.href): string {
const urlObj = new URL(url);
return urlObj.pathname;
}
/**
* 获取当前URL的主机名
* @param url URL字符串默认为当前URL
* @returns 主机名
*/
export function getHostname(url: string = window.location.href): string {
const urlObj = new URL(url);
return urlObj.hostname;
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2018", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}