jdk 17
This commit is contained in:
60
sdk/backend/oauth2-login-sdk/.gitignore
vendored
Normal file
60
sdk/backend/oauth2-login-sdk/.gitignore
vendored
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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"));
|
||||
// 处理时间字段
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
60
sdk/frontend/unified-login-sdk/.gitignore
vendored
Normal file
60
sdk/frontend/unified-login-sdk/.gitignore
vendored
Normal 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
|
||||
119
sdk/frontend/unified-login-sdk/README.md
Normal file
119
sdk/frontend/unified-login-sdk/README.md
Normal 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
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "oauth2-login-sdk",
|
||||
"name": "unified-login-sdk",
|
||||
"version": "1.0.0",
|
||||
"description": "TypeScript前端SDK,用于前后端分离项目对接统一登录系统",
|
||||
"main": "dist/index.js",
|
||||
@@ -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||[];
|
||||
|
||||
// 检查是否有所有权限
|
||||
287
sdk/frontend/unified-login-sdk/src/core/http.examples.ts
Normal file
287
sdk/frontend/unified-login-sdk/src/core/http.examples.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
136
sdk/frontend/unified-login-sdk/src/core/http.test.ts
Normal file
136
sdk/frontend/unified-login-sdk/src/core/http.test.ts
Normal 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('工具函数', () => {
|
||||
// 这里可以添加对私有方法的测试(如果需要的话)
|
||||
// 通常通过公共接口间接测试私有逻辑
|
||||
});
|
||||
655
sdk/frontend/unified-login-sdk/src/core/http.ts
Normal file
655
sdk/frontend/unified-login-sdk/src/core/http.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
92
sdk/frontend/unified-login-sdk/src/core/token.ts
Normal file
92
sdk/frontend/unified-login-sdk/src/core/token.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
281
sdk/frontend/unified-login-sdk/src/guards/router.examples.ts
Normal file
281
sdk/frontend/unified-login-sdk/src/guards/router.examples.ts
Normal 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 };
|
||||
}
|
||||
358
sdk/frontend/unified-login-sdk/src/guards/router.test.ts
Normal file
358
sdk/frontend/unified-login-sdk/src/guards/router.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
311
sdk/frontend/unified-login-sdk/src/guards/router.ts
Normal file
311
sdk/frontend/unified-login-sdk/src/guards/router.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
sdk/frontend/unified-login-sdk/src/index.ts
Normal file
108
sdk/frontend/unified-login-sdk/src/index.ts
Normal 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;
|
||||
}
|
||||
260
sdk/frontend/unified-login-sdk/src/plugins/vue.test.ts
Normal file
260
sdk/frontend/unified-login-sdk/src/plugins/vue.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
@@ -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>;
|
||||
|
||||
/**
|
||||
* 检查用户是否已认证
|
||||
Reference in New Issue
Block a user