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

60
sdk/backend/oauth2-login-sdk/.gitignore vendored Normal file
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

@@ -8,7 +8,7 @@
<version>1.0-SNAPSHOT</version>
<groupId>org.lingniu</groupId>
<packaging>jar</packaging>
<name>OAuth2 Login SDK</name>
<name>oauth2-login-sdk</name>
<description>OAuth2登录SDK后端Java版本</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>

View File

@@ -116,7 +116,7 @@ public class AccessTokenInfo {
return null;
}
AccessTokenInfo.AccessTokenInfoBuilder builder = AccessTokenInfo.builder();
AccessTokenInfoBuilder builder = AccessTokenInfo.builder();
builder.username((String) map.get("username"));
builder.tokenValue((String) map.get("tokenValue"));
// 处理时间字段

View File

@@ -1,70 +0,0 @@
## 安装
```bash
npm install oauth2-login-sdk --save
# 或
yarn add oauth2-login-sdk
```
## 快速开始
### 基本使用
```typescript
// main.ts
import unifiedLoginSDK from "oauth2-login-sdk"
// 初始化配置
unifiedLoginSDK.init({
clientId: import.meta.env.VITE_APP_CLIENT_ID,
registrationId: import.meta.env.VITE_APP_REGISTRATION_ID,
storageType: import.meta.env.VITE_APP_STORAGE_TYPE,
basepath: import.meta.env.VITE_APP_BASE_API,
idpLogoutUrl: import.meta.env.VITE_APP_IDP_LOGOUT_URL,
homePage: import.meta.env.VITE_APP_HOME_PAGE
})
```
```properties
# 配置文件
VITE_APP_CLIENT_ID=xxx
VITE_APP_REGISTRATION_ID=xxx
VITE_APP_STORAGE_TYPE=localStorage
VITE_APP_IDP_LOGOUT_URL=http://106.14.217.120/idp-ui/logout
VITE_APP_HOME_PAGE=http://106.14.217.120/portal-ui/index
```
```typescript
// 配置路由导航守卫
router.beforeEach(async (to, _from, next) => {
// 打开页面 判断是已认证
if (!unifiedLoginSDK.isAuthenticated()) {
// 未认证
if (to.path === '/oauth2/callback') {
// 如果是登录回调 进行回调登录
await unifiedLoginSDK.handleCallback()
}else{
// 跳转登录
await unifiedLoginSDK.login()
}
} else {
//已认证 打开页面
next()
}
})
```
```typescript
// 请求后端接口添加token
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000
})
// request拦截器
import unifiedLoginSDK from "oauth2-login-sdk"
service.interceptors.request.use((config: any) => {
if (getToken() && !isToken) {
config.headers['Authorization'] = unifiedLoginSDK.getToken()
}
})
```

View File

@@ -1,370 +0,0 @@
/**
* 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 class HttpError extends Error {
/** 状态码 */
public status: number;
/** 状态文本 */
public statusText: string;
/** 错误数据 */
public data: any;
/**
* 构造函数
* @param message 错误信息
* @param status 状态码
* @param statusText 状态文本
* @param data 错误数据
*/
constructor(message: string, status: number, statusText: string, data: any) {
super(message);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.data = data;
}
}
/**
* HTTP客户端类
*/
export class HttpClient {
private tokenGetter?: () => string | null;
private tenantId?: string;
/**
* 构造函数
* @param logout
* @param tokenGetter Token获取函数
*/
constructor(tokenGetter?: () => string | null) {
this.tokenGetter = tokenGetter;
}
/**
* 设置Token获取函数
* @param tokenGetter Token获取函数
*/
setTokenGetter(tokenGetter: () => string | null): void {
this.tokenGetter = tokenGetter;
}
/**
* 设置租户ID
* @param tenantId 租户ID
*/
setTenantId(tenantId?: string): void {
this.tenantId = tenantId;
}
/**
* 发送HTTP请求
* @param options 请求选项
* @returns Promise<HttpResponse<T>> 响应结果
*/
async request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
const {
method,
url,
headers = {},
body,
needAuth = true
} = options;
// 构建请求头
const requestHeaders: Record<string, string> = {
'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: RequestInit = {
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: '' as T,
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> 处理后的响应
*/
private handleResponse<T = any>(response: Response, responseData: any): HttpResponse<T> {
// 检查是否为业务响应结构
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 as T,
headers: this.parseHeaders(response.headers)
};
}
// 非业务响应结构,直接返回原始数据
return {
status: response.status,
statusText: response.statusText,
data: responseData as T,
headers: this.parseHeaders(response.headers)
};
}
/**
* 检查是否为业务响应结构
* @param responseData 响应数据
* @returns boolean 是否为业务响应结构
*/
private isBusinessResponse(responseData: any): boolean {
return typeof responseData === 'object' &&
responseData !== null &&
('code' in responseData) &&
('msg' in responseData) &&
('data' in responseData);
}
/**
* 获取错误信息
* @param responseData 响应数据
* @returns string 错误信息
*/
private getErrorMessage(responseData: any): string {
// 如果是业务响应结构
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<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
});
}
/**
* 解析响应体
* @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

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

View File

@@ -1,130 +0,0 @@
/**
* 路由守卫模块
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
*/
import { Auth } from '../core/auth';
/**
* 路由守卫选项
*/
export interface RouterGuardOptions {
/**
* 是否需要登录
*/
requiresAuth?: boolean;
/**
* 需要的权限列表
*/
requiredPermissions?: string[];
/**
* 登录后重定向的URL
*/
redirectUri?: string;
/**
* 权限不足时重定向的URL
*/
unauthorizedRedirectUri?: string;
}
/**
* 路由守卫类
*/
export class RouterGuard {
private auth: Auth;
/**
* 构造函数
* @param auth 认证实例
*/
constructor(auth: Auth) {
this.auth = auth;
}
/**
* 检查路由权限
* @param options 路由守卫选项
* @returns Promise<boolean> 是否通过权限检查
*/
async check(options: RouterGuardOptions): Promise<boolean> {
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: any, from: any, next: any) => {
// 从路由元信息中获取守卫选项
const options: RouterGuardOptions = to.meta?.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: string | string[]): Promise<boolean> {
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)
);
}
}

View File

@@ -1,94 +0,0 @@
/**
* 统一登录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';
// 导出Vue插件
export { VuePlugin, createVuePlugin } from './plugins/vue';
// 创建默认SDK实例
import { SDKConfig, UnifiedLoginSDK } from './types';
import { Auth as AuthCore } from './core/auth';
import { Storage as StorageCore } from './utils/storage';
/**
* 默认SDK实例
*/
const defaultStorage = new StorageCore();
const defaultAuth = new AuthCore(defaultStorage);
/**
* 默认导出的SDK实例
*/
export const unifiedLoginSDK: UnifiedLoginSDK = {
init: (config: SDKConfig) => {
defaultAuth.init(config);
},
getToken: () => {
return defaultAuth.getToken()
},
login: (redirectUri?: string) => {
return defaultAuth.login(redirectUri);
},
logout: () => {
return defaultAuth.logout();
},
handleCallback: () => {
return defaultAuth.handleCallback();
},
getRoutes: () => {
return defaultAuth.getRoutes();
},
getUserInfo: () => {
return defaultAuth.getUserInfo();
},
isAuthenticated: () => {
return defaultAuth.isAuthenticated();
},
hasRole: (role: string | string[]) => {
return defaultAuth.hasRole(role);
},
hasAllRoles: (roles: string[]) => {
return defaultAuth.hasAllRoles(roles);
},
hasPermission: (permission: string | string[]) => {
return defaultAuth.hasPermission(permission);
},
hasAllPermissions: (permissions: string[]) => {
return defaultAuth.hasAllPermissions(permissions);
},
on: (event, callback) => {
return defaultAuth.on(event, callback);
},
off: (event, callback) => {
return defaultAuth.off(event, callback);
},
isCallback: () => {
return defaultAuth.isCallback();
}
};
// 默认导出
export default unifiedLoginSDK;
// 版本信息
export const version = '1.0.0';

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

@@ -1,5 +1,5 @@
{
"name": "oauth2-login-sdk",
"name": "unified-login-sdk",
"version": "1.0.0",
"description": "TypeScript前端SDK用于前后端分离项目对接统一登录系统",
"main": "dist/index.js",

View File

@@ -14,9 +14,9 @@ import {buildQueryParams, isCallbackUrl, parseQueryParams} from '../utils/url';
*/
export class Auth {
private config: SDKConfig | null = null;
private tokenManager: TokenManager;
private tokenManager!: TokenManager;
private httpClient: HttpClient;
private storage: Storage;
private readonly storage: Storage;
private eventHandlers: Record<EventType, Function[]> = {
login: [],
logout: [],
@@ -30,10 +30,11 @@ export class Auth {
*/
constructor(storage: Storage) {
this.storage = storage;
// 创建HttpClient初始时tokenManager为undefined
this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);
// 然后创建TokenManager
this.tokenManager = new TokenManager(storage);
// 创建带有基础配置的HttpClient
this.httpClient = new HttpClient({
timeout: 15000,
withCredentials: true
});
}
/**
@@ -42,8 +43,18 @@ export class Auth {
*/
init(config: SDKConfig): void {
this.config = config;
// 设置租户ID到HTTP客户端
this.httpClient.setTenantId(config.tenantId);
// 更新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{
@@ -59,12 +70,20 @@ export class Auth {
throw new Error('SDK not initialized');
}
const registrationId = this.config.registrationId || 'idp'
const basepath = this.config.basepath || ''
const path = `${basepath}/oauth2/authorization/${registrationId}`
const path = `/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)
// 安全存储当前页面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
}
@@ -75,12 +94,10 @@ export class Auth {
if (!this.config) {
throw new Error('SDK not initialized');
}
// 清除本地存储的Token和用户信息
// 清除本地存储的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})
await this.httpClient.post(`/logout`,null,{needAuth:true})
// 触发退出事件
this.emit('logout');
window.location.href = this.config.idpLogoutUrl+'?redirect='+this.config.homePage;
@@ -108,8 +125,7 @@ export class Auth {
}
const registrationId = this.config.registrationId || 'idp'
const basepath = this.config.basepath || ''
const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`
const callback = `/login/oauth2/code/${registrationId}${buildQueryParams(params)}`
const tokenResponse = await this.httpClient.get(callback,{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
@@ -118,13 +134,27 @@ export class Auth {
})
// 触发登录事件
this.emit('login');
this.storage.set('userInfo', tokenResponse.data.data);
// 缓存用户信息
this.userInfoCache = 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;
// 安全处理重定向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);
}
window.location.href = url;
// 确保URL格式正确
if (!redirectUrl.startsWith('http') && !redirectUrl.startsWith('/')) {
redirectUrl = '/' + redirectUrl;
}
console.log('🔄 重定向到:', redirectUrl);
window.location.href = redirectUrl;
}
@@ -132,20 +162,50 @@ export class Auth {
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})
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
*/
getUserInfo(): UserInfo {
return this.storage.get("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;
}
}
@@ -161,7 +221,7 @@ export class Auth {
return false;
}
const userInfo:UserInfo = this.storage.get("userInfo");
const userInfo = await this.getUserInfo();
const roleCodes = userInfo.roles||[];
if (Array.isArray(role)) {
@@ -183,7 +243,7 @@ export class Auth {
return false;
}
const userInfo:UserInfo = this.storage.get("userInfo");
const userInfo = await this.getUserInfo();
const roleCodes = userInfo.roles||[];
// 检查是否有所有角色
return roles.every(r => roleCodes.includes(r));
@@ -199,7 +259,7 @@ export class Auth {
return false;
}
const userInfo:UserInfo = this.storage.get("userInfo");
const userInfo = await this.getUserInfo();
const permissions = userInfo.permissions||[];
if (Array.isArray(permission)) {
@@ -221,7 +281,7 @@ export class Auth {
return false;
}
const userInfo:UserInfo = this.storage.get("userInfo");
const userInfo = await this.getUserInfo();
const userPermissions = userInfo.permissions||[];
// 检查是否有所有权限

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

@@ -4,10 +4,15 @@
*/
import { Auth } from '../core/auth';
import { SDKConfig } from '../types';
import { SDKConfig, UnifiedLoginSDK } from '../types';
import { Storage } from '../utils/storage';
import { RouterGuard } from '../guards/router';
/**
* Vue版本类型
*/
type VueVersion = 'vue2' | 'vue3';
/**
* Vue插件选项
*/
@@ -26,14 +31,20 @@ export interface VuePluginOptions {
* Vue插件类
*/
export class VuePlugin {
private auth: Auth;
private routerGuard: RouterGuard;
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);
}
@@ -108,14 +119,67 @@ export class VuePlugin {
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'): VuePlugin {
const storage = new Storage(storageType);
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

@@ -16,24 +16,6 @@ export interface SDKConfig {
tenantId?: string;
}
/**
* Token信息
*/
export interface TokenInfo {
/** 访问令牌 */
accessToken: string;
/** 刷新令牌 */
refreshToken: string;
/** 令牌类型默认Bearer */
tokenType?: string;
/** 访问令牌过期时间(秒) */
expiresIn: number;
/** 刷新令牌过期时间(秒) */
refreshExpiresIn?: number;
/** 令牌颁发时间戳 */
issuedAt: number;
}
/**
*
*/

View File

@@ -36,7 +36,13 @@ export interface UnifiedLoginSDK {
*
* @returns Promise<UserInfo>
*/
getUserInfo(): import('./user').UserInfo;
getUserInfo(): Promise<import('./user').UserInfo>;
/**
*
* @returns Promise<UserInfo>
*/
refreshUserInfo(): Promise<import('./user').UserInfo>;
/**
*