1193 lines
34 KiB
JavaScript
1193 lines
34 KiB
JavaScript
/**
|
||
* Token管理模块
|
||
* 负责Token的存储、获取、刷新和过期处理
|
||
*/
|
||
/**
|
||
* Token管理类
|
||
*/
|
||
class TokenManager {
|
||
/**
|
||
* 构造函数
|
||
* @param storage 存储实例
|
||
* @param httpClient HTTP客户端实例
|
||
*/
|
||
constructor(storage) {
|
||
this.storage = storage;
|
||
}
|
||
/**
|
||
* 存储Token信息
|
||
* @param tokenInfo Token信息
|
||
*/
|
||
saveToken(tokenInfo) {
|
||
this.storage.set('token', tokenInfo);
|
||
}
|
||
/**
|
||
* 获取Token信息
|
||
* @returns TokenInfo | null Token信息
|
||
*/
|
||
getToken() {
|
||
return this.storage.get('token');
|
||
}
|
||
/**
|
||
* 清除Token信息
|
||
*/
|
||
clearToken() {
|
||
this.storage.remove('token');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* HTTP客户端
|
||
* 用于与后端API进行通信
|
||
*/
|
||
/**
|
||
* HTTP错误类型
|
||
*/
|
||
class HttpError extends Error {
|
||
/**
|
||
* 构造函数
|
||
* @param message 错误信息
|
||
* @param status 状态码
|
||
* @param statusText 状态文本
|
||
* @param data 错误数据
|
||
*/
|
||
constructor(message, status, statusText, data) {
|
||
super(message);
|
||
this.name = 'HttpError';
|
||
this.status = status;
|
||
this.statusText = statusText;
|
||
this.data = data;
|
||
}
|
||
}
|
||
/**
|
||
* HTTP客户端类
|
||
*/
|
||
class HttpClient {
|
||
/**
|
||
* 构造函数
|
||
* @param logout
|
||
* @param tokenGetter Token获取函数
|
||
*/
|
||
constructor(tokenGetter) {
|
||
this.tokenGetter = tokenGetter;
|
||
}
|
||
/**
|
||
* 设置Token获取函数
|
||
* @param tokenGetter Token获取函数
|
||
*/
|
||
setTokenGetter(tokenGetter) {
|
||
this.tokenGetter = tokenGetter;
|
||
}
|
||
/**
|
||
* 设置租户ID
|
||
* @param tenantId 租户ID
|
||
*/
|
||
setTenantId(tenantId) {
|
||
this.tenantId = tenantId;
|
||
}
|
||
/**
|
||
* 发送HTTP请求
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async request(options) {
|
||
const { method, url, headers = {}, body, needAuth = true } = options;
|
||
// 构建请求头
|
||
const requestHeaders = {
|
||
'Content-Type': 'application/json',
|
||
...headers
|
||
};
|
||
// 添加认证头
|
||
const addAuthHeader = () => {
|
||
if (needAuth && this.tokenGetter) {
|
||
const token = this.tokenGetter();
|
||
if (token) {
|
||
requestHeaders.Authorization = `${token}`;
|
||
}
|
||
}
|
||
};
|
||
// 添加租户ID头
|
||
if (this.tenantId) {
|
||
requestHeaders['tenant-id'] = this.tenantId;
|
||
}
|
||
addAuthHeader();
|
||
// 构建请求配置
|
||
const fetchOptions = {
|
||
method,
|
||
headers: requestHeaders,
|
||
credentials: 'include' // 包含cookie
|
||
};
|
||
// 添加请求体
|
||
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
||
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||
}
|
||
try {
|
||
// 发送请求
|
||
const response = await fetch(url, fetchOptions);
|
||
const responseData = await this.parseResponse(response);
|
||
// 检查响应状态
|
||
if (!response.ok) {
|
||
// 如果是401错误,尝试刷新Token并重试
|
||
if (response.status === 401) {
|
||
return {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
data: '',
|
||
headers: this.parseHeaders(response.headers)
|
||
};
|
||
}
|
||
// 其他错误,直接抛出
|
||
const errorMsg = this.getErrorMessage(responseData);
|
||
throw new HttpError(errorMsg, response.status, response.statusText, responseData);
|
||
}
|
||
// 处理成功响应的业务逻辑
|
||
return this.handleResponse(response, responseData);
|
||
}
|
||
catch (error) {
|
||
if (error instanceof HttpError) {
|
||
throw error;
|
||
}
|
||
// 网络错误或其他错误
|
||
throw new HttpError(error instanceof Error ? error.message : 'Network Error', 0, 'Network Error', null);
|
||
}
|
||
}
|
||
/**
|
||
* 处理响应数据
|
||
* @param response 响应对象
|
||
* @param responseData 响应数据
|
||
* @returns HttpResponse<T> 处理后的响应
|
||
*/
|
||
handleResponse(response, responseData) {
|
||
// 检查是否为业务响应结构
|
||
if (this.isBusinessResponse(responseData)) {
|
||
// 业务响应结构:{ code, msg, data }
|
||
const { code, msg, data } = responseData;
|
||
// 检查业务状态码
|
||
if (code !== 0 && code !== 200 && code !== '0' && code !== '200') {
|
||
// 业务错误,抛出HttpError
|
||
throw new HttpError(msg || `Business Error: ${code}`, response.status, response.statusText, responseData);
|
||
}
|
||
// 业务成功,返回data字段作为实际数据
|
||
return {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
data: data,
|
||
headers: this.parseHeaders(response.headers)
|
||
};
|
||
}
|
||
// 非业务响应结构,直接返回原始数据
|
||
return {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
data: responseData,
|
||
headers: this.parseHeaders(response.headers)
|
||
};
|
||
}
|
||
/**
|
||
* 检查是否为业务响应结构
|
||
* @param responseData 响应数据
|
||
* @returns boolean 是否为业务响应结构
|
||
*/
|
||
isBusinessResponse(responseData) {
|
||
return typeof responseData === 'object' &&
|
||
responseData !== null &&
|
||
('code' in responseData) &&
|
||
('msg' in responseData) &&
|
||
('data' in responseData);
|
||
}
|
||
/**
|
||
* 获取错误信息
|
||
* @param responseData 响应数据
|
||
* @returns string 错误信息
|
||
*/
|
||
getErrorMessage(responseData) {
|
||
// 如果是业务响应结构
|
||
if (this.isBusinessResponse(responseData)) {
|
||
return responseData.msg || `Business Error: ${responseData.code}`;
|
||
}
|
||
// 其他错误结构
|
||
return responseData.message || responseData.error || `HTTP Error`;
|
||
}
|
||
/**
|
||
* GET请求
|
||
* @param url 请求URL
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async get(url, options) {
|
||
return this.request({
|
||
method: 'GET',
|
||
url,
|
||
...options
|
||
});
|
||
}
|
||
/**
|
||
* POST请求
|
||
* @param url 请求URL
|
||
* @param body 请求体
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async post(url, body, options) {
|
||
return this.request({
|
||
method: 'POST',
|
||
url,
|
||
body,
|
||
...options
|
||
});
|
||
}
|
||
/**
|
||
* PUT请求
|
||
* @param url 请求URL
|
||
* @param body 请求体
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async put(url, body, options) {
|
||
return this.request({
|
||
method: 'PUT',
|
||
url,
|
||
body,
|
||
...options
|
||
});
|
||
}
|
||
/**
|
||
* DELETE请求
|
||
* @param url 请求URL
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async delete(url, options) {
|
||
return this.request({
|
||
method: 'DELETE',
|
||
url,
|
||
...options
|
||
});
|
||
}
|
||
/**
|
||
* PATCH请求
|
||
* @param url 请求URL
|
||
* @param body 请求体
|
||
* @param options 请求选项
|
||
* @returns Promise<HttpResponse<T>> 响应结果
|
||
*/
|
||
async patch(url, body, options) {
|
||
return this.request({
|
||
method: 'PATCH',
|
||
url,
|
||
body,
|
||
...options
|
||
});
|
||
}
|
||
/**
|
||
* 解析响应体
|
||
* @param response 响应对象
|
||
* @returns Promise<any> 解析后的响应体
|
||
*/
|
||
async parseResponse(response) {
|
||
const contentType = response.headers.get('content-type') || '';
|
||
if (contentType.includes('application/json')) {
|
||
return response.json();
|
||
}
|
||
else if (contentType.includes('text/')) {
|
||
return response.text();
|
||
}
|
||
else {
|
||
return response.blob();
|
||
}
|
||
}
|
||
/**
|
||
* 解析响应头
|
||
* @param headers 响应头对象
|
||
* @returns Record<string, string> 解析后的响应头
|
||
*/
|
||
parseHeaders(headers) {
|
||
const result = {};
|
||
headers.forEach((value, key) => {
|
||
result[key] = value;
|
||
});
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* URL处理工具
|
||
* 用于生成授权URL、解析URL参数等功能
|
||
*/
|
||
/**
|
||
* 生成随机字符串
|
||
* @param length 字符串长度,默认32位
|
||
* @returns 随机字符串
|
||
*/
|
||
function generateRandomString(length = 32) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
let result = '';
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* 解析URL查询参数
|
||
* @param url URL字符串,默认为当前URL
|
||
* @returns 查询参数对象
|
||
*/
|
||
function parseQueryParams(url = window.location.href) {
|
||
const params = {};
|
||
const queryString = url.split('?')[1];
|
||
if (!queryString) {
|
||
return params;
|
||
}
|
||
const pairs = queryString.split('&');
|
||
for (const pair of pairs) {
|
||
const [key, value] = pair.split('=');
|
||
if (key) {
|
||
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
||
}
|
||
}
|
||
return params;
|
||
}
|
||
/**
|
||
* 构建URL查询参数
|
||
* @param params 查询参数对象
|
||
* @returns 查询参数字符串
|
||
*/
|
||
function buildQueryParams(params) {
|
||
const pairs = [];
|
||
for (const [key, value] of Object.entries(params)) {
|
||
if (value !== undefined && value !== null) {
|
||
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||
}
|
||
}
|
||
return pairs.length ? `?${pairs.join('&')}` : '';
|
||
}
|
||
/**
|
||
* 生成OAuth2授权URL
|
||
* @param authorizationEndpoint 授权端点URL
|
||
* @param clientId 客户端ID
|
||
* @param redirectUri 重定向URL
|
||
* @param options 可选参数
|
||
* @returns 授权URL
|
||
*/
|
||
function generateAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, options) {
|
||
const { responseType = 'code', scope, state = generateRandomString(32), ...extraParams } = options || {};
|
||
const params = {
|
||
client_id: clientId,
|
||
redirect_uri: redirectUri,
|
||
response_type: responseType,
|
||
state,
|
||
...(scope ? { scope } : {}),
|
||
...extraParams
|
||
};
|
||
const queryString = buildQueryParams(params);
|
||
return `${authorizationEndpoint}${queryString}`;
|
||
}
|
||
/**
|
||
* 检查当前URL是否为授权回调
|
||
* @param url URL字符串,默认为当前URL
|
||
* @returns 是否为授权回调
|
||
*/
|
||
function isCallbackUrl(url = window.location.href) {
|
||
const params = parseQueryParams(url);
|
||
return !!params.code || !!params.error;
|
||
}
|
||
|
||
/**
|
||
* 认证核心逻辑
|
||
* 实现OAuth2授权码模式的完整流程
|
||
*/
|
||
/**
|
||
* 认证核心类
|
||
*/
|
||
class Auth {
|
||
/**
|
||
* 构造函数
|
||
* @param storage 存储实例
|
||
*/
|
||
constructor(storage) {
|
||
this.config = null;
|
||
this.eventHandlers = {
|
||
login: [],
|
||
logout: [],
|
||
tokenExpired: []
|
||
};
|
||
this.userInfoCache = null;
|
||
this.storage = storage;
|
||
// 先创建HttpClient,初始时tokenManager为undefined
|
||
this.httpClient = new HttpClient(() => this.tokenManager.getToken() || null);
|
||
// 然后创建TokenManager
|
||
this.tokenManager = new TokenManager(storage);
|
||
}
|
||
/**
|
||
* 初始化SDK配置
|
||
* @param config SDK配置选项
|
||
*/
|
||
init(config) {
|
||
this.config = config;
|
||
// 设置租户ID到HTTP客户端
|
||
this.httpClient.setTenantId(config.tenantId);
|
||
}
|
||
getToken() {
|
||
return this.tokenManager.getToken();
|
||
}
|
||
/**
|
||
* 触发登录流程
|
||
* @param redirectUri 可选的重定向URL,覆盖初始化时的配置
|
||
*/
|
||
async login(redirectUri) {
|
||
if (!this.config) {
|
||
throw new Error('SDK not initialized');
|
||
}
|
||
const registrationId = this.config.registrationId || 'idp';
|
||
const basepath = this.config.basepath || '';
|
||
const path = `${basepath}/oauth2/authorization/${registrationId}`;
|
||
const tokenResponse = await this.httpClient.get(path, { needAuth: false });
|
||
const redirect = tokenResponse.data.redirect_url;
|
||
const params = parseQueryParams(redirect);
|
||
this.storage.set(params.state, window.location.href);
|
||
window.location.href = redirect;
|
||
}
|
||
/**
|
||
* 退出登录
|
||
*/
|
||
async logout() {
|
||
if (!this.config) {
|
||
throw new Error('SDK not initialized');
|
||
}
|
||
// 清除本地存储的Token和用户信息
|
||
this.tokenManager.clearToken();
|
||
this.userInfoCache = null;
|
||
this.storage.remove('userInfo');
|
||
const basepath = this.config.basepath || '';
|
||
await this.httpClient.post(`${basepath}/logout`, null, { needAuth: true });
|
||
// 触发退出事件
|
||
this.emit('logout');
|
||
window.location.href = this.config.idpLogoutUrl + '?redirect=' + this.config.homePage;
|
||
}
|
||
/**
|
||
* 处理授权回调
|
||
* @returns Promise<UserInfo> 用户信息
|
||
*/
|
||
async handleCallback() {
|
||
if (!this.config) {
|
||
throw new Error('SDK not initialized');
|
||
}
|
||
const params = parseQueryParams();
|
||
// 检查是否有错误
|
||
if (params.error) {
|
||
throw new Error(`Authorization error: ${params.error} - ${params.error_description || ''}`);
|
||
}
|
||
// 检查是否有授权码
|
||
if (!params.code) {
|
||
throw new Error('Authorization code not found');
|
||
}
|
||
const registrationId = this.config.registrationId || 'idp';
|
||
const basepath = this.config.basepath || '';
|
||
const callback = `${basepath}/login/oauth2/code/${registrationId}${buildQueryParams(params)}`;
|
||
const tokenResponse = await this.httpClient.get(callback, {
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
needAuth: false
|
||
});
|
||
// 触发登录事件
|
||
this.emit('login');
|
||
this.storage.set('userInfo', tokenResponse.data.data);
|
||
this.tokenManager.saveToken(tokenResponse.headers['authorization'] || tokenResponse.headers['Authorization']);
|
||
let url = this.config.homePage;
|
||
if (params.state) {
|
||
url = this.storage.get(params.state) || url;
|
||
}
|
||
window.location.href = url;
|
||
}
|
||
async getRoutes() {
|
||
if (!this.config) {
|
||
throw new Error('SDK not initialized');
|
||
}
|
||
const basepath = this.config.basepath || '';
|
||
const tokenResponse = await this.httpClient.get(`${basepath}/idp/routes`, { needAuth: true });
|
||
if (tokenResponse.status === 401) {
|
||
await this.logout();
|
||
}
|
||
return tokenResponse.data.data;
|
||
}
|
||
/**
|
||
* 获取用户信息
|
||
* @returns UserInfo 用户信息
|
||
*/
|
||
getUserInfo() {
|
||
return this.storage.get("userInfo");
|
||
}
|
||
/**
|
||
* 检查用户是否有指定角色
|
||
* @param role 角色编码或角色编码列表
|
||
* @returns Promise<boolean> 是否有指定角色
|
||
*/
|
||
async hasRole(role) {
|
||
if (!this.isAuthenticated()) {
|
||
return false;
|
||
}
|
||
const userInfo = this.storage.get("userInfo");
|
||
const roleCodes = userInfo.roles || [];
|
||
if (Array.isArray(role)) {
|
||
// 检查是否有任一角色
|
||
return role.some(r => roleCodes.includes(r));
|
||
}
|
||
// 检查是否有单个角色
|
||
return roleCodes.includes(role);
|
||
}
|
||
/**
|
||
* 检查用户是否有所有指定角色
|
||
* @param roles 角色编码列表
|
||
* @returns Promise<boolean> 是否有所有指定角色
|
||
*/
|
||
async hasAllRoles(roles) {
|
||
if (!this.isAuthenticated()) {
|
||
return false;
|
||
}
|
||
const userInfo = this.storage.get("userInfo");
|
||
const roleCodes = userInfo.roles || [];
|
||
// 检查是否有所有角色
|
||
return roles.every(r => roleCodes.includes(r));
|
||
}
|
||
/**
|
||
* 检查用户是否有指定权限
|
||
* @param permission 权限标识或权限标识列表
|
||
* @returns Promise<boolean> 是否有指定权限
|
||
*/
|
||
async hasPermission(permission) {
|
||
if (!this.isAuthenticated()) {
|
||
return false;
|
||
}
|
||
const userInfo = this.storage.get("userInfo");
|
||
const permissions = userInfo.permissions || [];
|
||
if (Array.isArray(permission)) {
|
||
// 检查是否有任一权限
|
||
return permission.some(p => permissions.includes(p));
|
||
}
|
||
// 检查是否有单个权限
|
||
return permissions.includes(permission);
|
||
}
|
||
/**
|
||
* 检查用户是否有所有指定权限
|
||
* @param permissions 权限标识列表
|
||
* @returns Promise<boolean> 是否有所有指定权限
|
||
*/
|
||
async hasAllPermissions(permissions) {
|
||
if (!this.isAuthenticated()) {
|
||
return false;
|
||
}
|
||
const userInfo = this.storage.get("userInfo");
|
||
const userPermissions = userInfo.permissions || [];
|
||
// 检查是否有所有权限
|
||
return permissions.every(p => userPermissions.includes(p));
|
||
}
|
||
/**
|
||
* 检查用户是否已认证
|
||
* @returns boolean 是否已认证
|
||
*/
|
||
isAuthenticated() {
|
||
// 检查Token是否存在且未过期
|
||
return !!this.tokenManager.getToken();
|
||
}
|
||
/**
|
||
* 事件监听
|
||
* @param event 事件类型
|
||
* @param callback 回调函数
|
||
*/
|
||
on(event, callback) {
|
||
this.eventHandlers[event].push(callback);
|
||
}
|
||
/**
|
||
* 移除事件监听
|
||
* @param event 事件类型
|
||
* @param callback 回调函数
|
||
*/
|
||
off(event, callback) {
|
||
this.eventHandlers[event] = this.eventHandlers[event].filter(handler => handler !== callback);
|
||
}
|
||
/**
|
||
* 触发事件
|
||
* @param event 事件类型
|
||
* @param data 事件数据
|
||
*/
|
||
emit(event, data) {
|
||
this.eventHandlers[event].forEach(handler => {
|
||
try {
|
||
handler(data);
|
||
}
|
||
catch (error) {
|
||
console.error(`Error in ${event} event handler:`, error);
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* 检查当前URL是否为授权回调
|
||
* @returns boolean 是否为授权回调
|
||
*/
|
||
isCallback() {
|
||
return isCallbackUrl();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 存储工具类
|
||
* 支持localStorage、sessionStorage和cookie三种存储方式
|
||
*/
|
||
/**
|
||
* 存储工具类
|
||
*/
|
||
class Storage {
|
||
/**
|
||
* 构造函数
|
||
* @param storageType 存储类型
|
||
* @param prefix 存储前缀,默认'unified_login_'
|
||
*/
|
||
constructor(storageType = 'localStorage', prefix = 'unified_login_') {
|
||
this.storageType = storageType;
|
||
this.prefix = prefix;
|
||
}
|
||
/**
|
||
* 设置存储项
|
||
* @param key 存储键
|
||
* @param value 存储值
|
||
* @param options 可选参数,cookie存储时使用
|
||
*/
|
||
set(key, value, options) {
|
||
const fullKey = this.prefix + key;
|
||
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
||
switch (this.storageType) {
|
||
case 'localStorage':
|
||
this.setLocalStorage(fullKey, stringValue);
|
||
break;
|
||
case 'sessionStorage':
|
||
this.setSessionStorage(fullKey, stringValue);
|
||
break;
|
||
case 'cookie':
|
||
this.setCookie(fullKey, stringValue, options);
|
||
break;
|
||
}
|
||
}
|
||
/**
|
||
* 获取存储项
|
||
* @param key 存储键
|
||
* @returns 存储值
|
||
*/
|
||
get(key) {
|
||
const fullKey = this.prefix + key;
|
||
let value;
|
||
switch (this.storageType) {
|
||
case 'localStorage':
|
||
value = this.getLocalStorage(fullKey);
|
||
break;
|
||
case 'sessionStorage':
|
||
value = this.getSessionStorage(fullKey);
|
||
break;
|
||
case 'cookie':
|
||
value = this.getCookie(fullKey);
|
||
break;
|
||
default:
|
||
value = null;
|
||
}
|
||
if (value === null) {
|
||
return null;
|
||
}
|
||
// 尝试解析JSON
|
||
try {
|
||
return JSON.parse(value);
|
||
}
|
||
catch (e) {
|
||
// 如果不是JSON,直接返回字符串
|
||
return value;
|
||
}
|
||
}
|
||
/**
|
||
* 移除存储项
|
||
* @param key 存储键
|
||
*/
|
||
remove(key) {
|
||
const fullKey = this.prefix + key;
|
||
switch (this.storageType) {
|
||
case 'localStorage':
|
||
this.removeLocalStorage(fullKey);
|
||
break;
|
||
case 'sessionStorage':
|
||
this.removeSessionStorage(fullKey);
|
||
break;
|
||
case 'cookie':
|
||
this.removeCookie(fullKey);
|
||
break;
|
||
}
|
||
}
|
||
/**
|
||
* 清空所有存储项
|
||
*/
|
||
clear() {
|
||
switch (this.storageType) {
|
||
case 'localStorage':
|
||
this.clearLocalStorage();
|
||
break;
|
||
case 'sessionStorage':
|
||
this.clearSessionStorage();
|
||
break;
|
||
case 'cookie':
|
||
this.clearCookie();
|
||
break;
|
||
}
|
||
}
|
||
/**
|
||
* 检查存储类型是否可用
|
||
* @returns boolean 是否可用
|
||
*/
|
||
isAvailable() {
|
||
try {
|
||
switch (this.storageType) {
|
||
case 'localStorage':
|
||
return this.isLocalStorageAvailable();
|
||
case 'sessionStorage':
|
||
return this.isSessionStorageAvailable();
|
||
case 'cookie':
|
||
return typeof document !== 'undefined';
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
// ------------------------ localStorage 操作 ------------------------
|
||
/**
|
||
* 设置localStorage
|
||
*/
|
||
setLocalStorage(key, value) {
|
||
if (this.isLocalStorageAvailable()) {
|
||
localStorage.setItem(key, value);
|
||
}
|
||
}
|
||
/**
|
||
* 获取localStorage
|
||
*/
|
||
getLocalStorage(key) {
|
||
if (this.isLocalStorageAvailable()) {
|
||
return localStorage.getItem(key);
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* 移除localStorage
|
||
*/
|
||
removeLocalStorage(key) {
|
||
if (this.isLocalStorageAvailable()) {
|
||
localStorage.removeItem(key);
|
||
}
|
||
}
|
||
/**
|
||
* 清空localStorage中所有带前缀的项
|
||
*/
|
||
clearLocalStorage() {
|
||
if (this.isLocalStorageAvailable()) {
|
||
for (let i = 0; i < localStorage.length; i++) {
|
||
const key = localStorage.key(i);
|
||
if (key && key.startsWith(this.prefix)) {
|
||
localStorage.removeItem(key);
|
||
i--; // 索引调整
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 检查localStorage是否可用
|
||
*/
|
||
isLocalStorageAvailable() {
|
||
if (typeof localStorage === 'undefined') {
|
||
return false;
|
||
}
|
||
try {
|
||
const testKey = '__storage_test__';
|
||
localStorage.setItem(testKey, testKey);
|
||
localStorage.removeItem(testKey);
|
||
return true;
|
||
}
|
||
catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
// ------------------------ sessionStorage 操作 ------------------------
|
||
/**
|
||
* 设置sessionStorage
|
||
*/
|
||
setSessionStorage(key, value) {
|
||
if (this.isSessionStorageAvailable()) {
|
||
sessionStorage.setItem(key, value);
|
||
}
|
||
}
|
||
/**
|
||
* 获取sessionStorage
|
||
*/
|
||
getSessionStorage(key) {
|
||
if (this.isSessionStorageAvailable()) {
|
||
return sessionStorage.getItem(key);
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* 移除sessionStorage
|
||
*/
|
||
removeSessionStorage(key) {
|
||
if (this.isSessionStorageAvailable()) {
|
||
sessionStorage.removeItem(key);
|
||
}
|
||
}
|
||
/**
|
||
* 清空sessionStorage中所有带前缀的项
|
||
*/
|
||
clearSessionStorage() {
|
||
if (this.isSessionStorageAvailable()) {
|
||
for (let i = 0; i < sessionStorage.length; i++) {
|
||
const key = sessionStorage.key(i);
|
||
if (key && key.startsWith(this.prefix)) {
|
||
sessionStorage.removeItem(key);
|
||
i--; // 索引调整
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 检查sessionStorage是否可用
|
||
*/
|
||
isSessionStorageAvailable() {
|
||
if (typeof sessionStorage === 'undefined') {
|
||
return false;
|
||
}
|
||
try {
|
||
const testKey = '__storage_test__';
|
||
sessionStorage.setItem(testKey, testKey);
|
||
sessionStorage.removeItem(testKey);
|
||
return true;
|
||
}
|
||
catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
// ------------------------ cookie 操作 ------------------------
|
||
/**
|
||
* 设置cookie
|
||
*/
|
||
setCookie(key, value, options) {
|
||
if (typeof document === 'undefined') {
|
||
return;
|
||
}
|
||
let cookieString = `${key}=${encodeURIComponent(value)}`;
|
||
if (options) {
|
||
// 设置过期时间(秒)
|
||
if (options.expires) {
|
||
const date = new Date();
|
||
date.setTime(date.getTime() + options.expires * 1000);
|
||
cookieString += `; expires=${date.toUTCString()}`;
|
||
}
|
||
// 设置路径
|
||
if (options.path) {
|
||
cookieString += `; path=${options.path}`;
|
||
}
|
||
// 设置域名
|
||
if (options.domain) {
|
||
cookieString += `; domain=${options.domain}`;
|
||
}
|
||
// 设置secure
|
||
if (options.secure) {
|
||
cookieString += '; secure';
|
||
}
|
||
}
|
||
document.cookie = cookieString;
|
||
}
|
||
/**
|
||
* 获取cookie
|
||
*/
|
||
getCookie(key) {
|
||
if (typeof document === 'undefined') {
|
||
return null;
|
||
}
|
||
const name = `${key}=`;
|
||
const decodedCookie = decodeURIComponent(document.cookie);
|
||
const ca = decodedCookie.split(';');
|
||
for (let i = 0; i < ca.length; i++) {
|
||
let c = ca[i];
|
||
while (c.charAt(0) === ' ') {
|
||
c = c.substring(1);
|
||
}
|
||
if (c.indexOf(name) === 0) {
|
||
return c.substring(name.length, c.length);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* 移除cookie
|
||
*/
|
||
removeCookie(key) {
|
||
this.setCookie(key, '', { expires: -1 });
|
||
}
|
||
/**
|
||
* 清空所有带前缀的cookie
|
||
*/
|
||
clearCookie() {
|
||
if (typeof document === 'undefined') {
|
||
return;
|
||
}
|
||
const cookies = document.cookie.split(';');
|
||
for (const cookie of cookies) {
|
||
const eqPos = cookie.indexOf('=');
|
||
const key = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
||
if (key.startsWith(this.prefix)) {
|
||
this.removeCookie(key);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 路由守卫模块
|
||
* 提供基于权限的路由拦截和未登录自动跳转登录页功能
|
||
*/
|
||
/**
|
||
* 路由守卫类
|
||
*/
|
||
class RouterGuard {
|
||
/**
|
||
* 构造函数
|
||
* @param auth 认证实例
|
||
*/
|
||
constructor(auth) {
|
||
this.auth = auth;
|
||
}
|
||
/**
|
||
* 检查路由权限
|
||
* @param options 路由守卫选项
|
||
* @returns Promise<boolean> 是否通过权限检查
|
||
*/
|
||
async check(options) {
|
||
const { requiresAuth = true, requiredPermissions = [] } = options;
|
||
// 检查是否需要登录
|
||
if (requiresAuth) {
|
||
// 检查是否已认证
|
||
if (!this.auth.isAuthenticated()) {
|
||
// 未认证,跳转到登录页
|
||
this.auth.login(options.redirectUri);
|
||
return false;
|
||
}
|
||
// 检查是否需要权限
|
||
if (requiredPermissions.length > 0) {
|
||
// 获取用户权限
|
||
const userPermissions = [''];
|
||
// 检查是否拥有所有需要的权限
|
||
const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission));
|
||
if (!hasPermission) {
|
||
// 权限不足,跳转到权限不足页
|
||
if (options.unauthorizedRedirectUri) {
|
||
window.location.href = options.unauthorizedRedirectUri;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
/**
|
||
* 创建Vue路由守卫
|
||
* @returns 路由守卫函数
|
||
*/
|
||
createVueGuard() {
|
||
return async (to, from, next) => {
|
||
var _a;
|
||
// 从路由元信息中获取守卫选项
|
||
const options = ((_a = to.meta) === null || _a === void 0 ? void 0 : _a.auth) || {};
|
||
try {
|
||
const allowed = await this.check(options);
|
||
if (allowed) {
|
||
next();
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('Route guard error:', error);
|
||
next(false);
|
||
}
|
||
};
|
||
}
|
||
/**
|
||
* 检查当前用户是否有权限访问资源
|
||
* @param permissions 需要的权限列表
|
||
* @returns Promise<boolean> 是否拥有权限
|
||
*/
|
||
async hasPermission(permissions) {
|
||
if (!permissions) {
|
||
return true;
|
||
}
|
||
const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];
|
||
// 检查是否已认证
|
||
if (!this.auth.isAuthenticated()) {
|
||
return false;
|
||
}
|
||
// 获取用户权限
|
||
const userPermissions = [''];
|
||
// 检查是否拥有所有需要的权限
|
||
return requiredPermissions.every(permission => userPermissions.includes(permission));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vue插件模块
|
||
* 提供Vue应用中使用统一登录SDK的能力
|
||
*/
|
||
/**
|
||
* Vue插件类
|
||
*/
|
||
class VuePlugin {
|
||
/**
|
||
* 构造函数
|
||
* @param storage 存储实例
|
||
*/
|
||
constructor(storage) {
|
||
this.auth = new Auth(storage);
|
||
this.routerGuard = new RouterGuard(this.auth);
|
||
}
|
||
/**
|
||
* 安装Vue插件
|
||
* @param app Vue构造函数或Vue 3应用实例
|
||
* @param options 插件选项
|
||
*/
|
||
install(app, options) {
|
||
const { config, pluginName = 'unifiedLogin' } = options;
|
||
// 初始化SDK
|
||
this.auth.init(config);
|
||
// 判断是Vue 2还是Vue 3
|
||
const isVue3 = typeof app.config !== 'undefined';
|
||
if (isVue3) {
|
||
// Vue 3
|
||
// 在全局属性上挂载SDK实例
|
||
app.config.globalProperties[`${pluginName}`] = this.auth;
|
||
app.config.globalProperties.$auth = this.auth; // 兼容简写
|
||
// 提供Vue组件内的注入
|
||
app.provide(pluginName, this.auth);
|
||
app.provide('auth', this.auth); // 兼容简写
|
||
// 处理路由守卫
|
||
app.mixin({
|
||
beforeCreate() {
|
||
// 如果是根组件,添加路由守卫
|
||
if (this.$options.router) {
|
||
const router = this.$options.router;
|
||
// 添加全局前置守卫
|
||
router.beforeEach(this.routerGuard.createVueGuard());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
// Vue 2
|
||
// 在Vue实例上挂载SDK实例
|
||
app.prototype[`${pluginName}`] = this.auth;
|
||
app.prototype.$auth = this.auth; // 兼容简写
|
||
// 全局混入
|
||
app.mixin({
|
||
beforeCreate() {
|
||
// 如果是根组件,添加路由守卫
|
||
if (this.$options.router) {
|
||
const router = this.$options.router;
|
||
// 添加全局前置守卫
|
||
router.beforeEach(this.routerGuard.createVueGuard());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
/**
|
||
* 获取认证实例
|
||
* @returns Auth 认证实例
|
||
*/
|
||
getAuth() {
|
||
return this.auth;
|
||
}
|
||
/**
|
||
* 获取路由守卫实例
|
||
* @returns RouterGuard 路由守卫实例
|
||
*/
|
||
getRouterGuard() {
|
||
return this.routerGuard;
|
||
}
|
||
}
|
||
/**
|
||
* 创建Vue插件实例
|
||
* @param storageType 存储类型
|
||
* @returns VuePlugin Vue插件实例
|
||
*/
|
||
function createVuePlugin(storageType) {
|
||
const storage = new Storage(storageType);
|
||
return new VuePlugin(storage);
|
||
}
|
||
|
||
/**
|
||
* 统一登录SDK入口文件
|
||
* 支持OAuth2授权码模式,提供完整的Token管理和用户信息管理功能
|
||
*/
|
||
// 导出核心类和功能
|
||
/**
|
||
* 默认SDK实例
|
||
*/
|
||
const defaultStorage = new Storage();
|
||
const defaultAuth = new Auth(defaultStorage);
|
||
/**
|
||
* 默认导出的SDK实例
|
||
*/
|
||
const unifiedLoginSDK = {
|
||
init: (config) => {
|
||
defaultAuth.init(config);
|
||
},
|
||
getToken: () => {
|
||
return defaultAuth.getToken();
|
||
},
|
||
login: (redirectUri) => {
|
||
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) => {
|
||
return defaultAuth.hasRole(role);
|
||
},
|
||
hasAllRoles: (roles) => {
|
||
return defaultAuth.hasAllRoles(roles);
|
||
},
|
||
hasPermission: (permission) => {
|
||
return defaultAuth.hasPermission(permission);
|
||
},
|
||
hasAllPermissions: (permissions) => {
|
||
return defaultAuth.hasAllPermissions(permissions);
|
||
},
|
||
on: (event, callback) => {
|
||
return defaultAuth.on(event, callback);
|
||
},
|
||
off: (event, callback) => {
|
||
return defaultAuth.off(event, callback);
|
||
},
|
||
isCallback: () => {
|
||
return defaultAuth.isCallback();
|
||
}
|
||
};
|
||
// 版本信息
|
||
const version = '1.0.0';
|
||
|
||
export { Auth, HttpClient, HttpError, RouterGuard, Storage, TokenManager, VuePlugin, buildQueryParams, createVuePlugin, unifiedLoginSDK as default, generateAuthorizationUrl, generateRandomString, isCallbackUrl, parseQueryParams, unifiedLoginSDK, version };
|
||
//# sourceMappingURL=index.esm.js.map
|