From bc70cbf087a7603a18d08543b91c4f5c31949ca3 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Tue, 18 Nov 2025 20:45:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A0=B8=E5=BF=83=E5=B7=A5=E5=85=B7=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增认证工具函数(auth.ts):Token管理、权限检查 - 新增 HTTP 请求封装(request.ts):Axios拦截器、响应处理 - 新增本地存储工具(storage.ts):统一的存储接口 --- src/utils/auth.ts | 216 ++++++++++++++++++++++++++++++++++++ src/utils/request.ts | 242 ++++++++++++++++++++++++++++++++++++++++ src/utils/storage.ts | 258 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 716 insertions(+) create mode 100644 src/utils/auth.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/storage.ts diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..9a082cb --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,216 @@ +/** + * 认证工具函数 + * @description 提供认证、权限检查等功能 + */ + +import { ROLE_ADMIN, UNAUTHORIZED_CODE } from '@/constants'; +import type { LocalUserInfo } from '@/types/auth'; +import { getToken, getUserInfo, getUserRole, clearAuthInfo } from './storage'; + +// ==================== 认证状态检查 ==================== + +/** + * 检查用户是否已登录 + * @returns 是否已登录 + */ +export function isAuthenticated(): boolean { + const token = getToken(); + return !!token; +} + +/** + * 检查是否需要登录 + * @returns 是否需要登录 + */ +export function requireAuth(): boolean { + return !isAuthenticated(); +} + +// ==================== 权限检查 ==================== + +/** + * 检查用户是否为管理员 + * @returns 是否为管理员 + */ +export function isAdmin(): boolean { + const userInfo = getUserInfo(); + if (!userInfo) { + return false; + } + return userInfo.role === ROLE_ADMIN; +} + +/** + * 检查用户是否具有指定角色 + * @param role - 角色名称 + * @returns 是否具有该角色 + */ +export function hasRole(role: string): boolean { + const userInfo = getUserInfo(); + if (!userInfo) { + return false; + } + return userInfo.role === role; +} + +/** + * 检查用户是否具有指定角色之一 + * @param roles - 角色名称数组 + * @returns 是否具有其中一个角色 + */ +export function hasAnyRole(roles: string[]): boolean { + const userInfo = getUserInfo(); + if (!userInfo) { + return false; + } + return roles.includes(userInfo.role); +} + +/** + * 检查用户是否具有所有指定角色 + * @param roles - 角色名称数组 + * @returns 是否具有所有角色 + */ +export function hasAllRoles(roles: string[]): boolean { + const userInfo = getUserInfo(); + if (!userInfo) { + return false; + } + // 注意:由于后端一个用户只有一个角色,这个函数实际上只在 roles 长度为 1 时返回 true + return roles.length === 1 && roles[0] === userInfo.role; +} + +/** + * 检查用户是否有权限访问 + * @param requiredRole - 需要的角色(可选) + * @returns 是否有权限 + */ +export function hasPermission(requiredRole?: string): boolean { + // 如果未指定角色要求,只需要已登录即可 + if (!requiredRole) { + return isAuthenticated(); + } + + // 检查是否具有指定角色 + return hasRole(requiredRole); +} + +// ==================== 获取认证信息 ==================== + +/** + * 获取当前用户信息 + * @returns 用户信息或 null + */ +export function getCurrentUser(): LocalUserInfo | null { + return getUserInfo(); +} + +/** + * 获取当前用户角色 + * @returns 用户角色或 null + */ +export function getCurrentUserRole(): string | null { + const userInfo = getUserInfo(); + return userInfo ? userInfo.role : null; +} + +/** + * 获取当前用户ID + * @returns 用户ID或 null + */ +export function getCurrentUserId(): number | null { + const userInfo = getUserInfo(); + return userInfo ? userInfo.userId : null; +} + +/** + * 获取当前用户名 + * @returns 用户名或 null + */ +export function getCurrentUsername(): string | null { + const userInfo = getUserInfo(); + return userInfo ? userInfo.username : null; +} + +// ==================== 认证请求头 ==================== + +/** + * 获取认证请求头 + * @returns 认证请求头对象 + */ +export function getAuthHeaders(): Record { + const token = getToken(); + if (!token) { + return {}; + } + + return { + Authorization: token, + }; +} + +// ==================== 登出处理 ==================== + +/** + * 处理用户登出 + * @param redirectToLogin - 是否跳转到登录页(默认 true) + */ +export function handleLogout(redirectToLogin = true): void { + // 清理所有认证信息 + clearAuthInfo(); + + // 跳转到登录页 + if (redirectToLogin) { + // 保存当前路径,登录后可以跳转回来 + const currentPath = window.location.pathname + window.location.search; + if (currentPath !== '/login') { + sessionStorage.setItem('redirectPath', currentPath); + } + + // 跳转到登录页 + window.location.href = '/login'; + } +} + +/** + * 处理未授权(401) + * @param message - 提示消息(可选) + */ +export function handleUnauthorized(message?: string): void { + console.warn('Unauthorized access:', message || 'Token expired or invalid'); + + // 清理认证信息并跳转登录页 + handleLogout(true); +} + +/** + * 处理无权限(403) + * @param message - 提示消息(可选) + */ +export function handleForbidden(message?: string): void { + console.warn('Forbidden access:', message || 'Insufficient permissions'); + + // 可以跳转到无权限提示页,或者显示错误提示 + // 这里简单处理,直接返回首页 + window.location.href = '/'; +} + +// ==================== 登录后重定向 ==================== + +/** + * 获取登录后的重定向路径 + * @returns 重定向路径 + */ +export function getRedirectPath(): string { + const redirectPath = sessionStorage.getItem('redirectPath'); + sessionStorage.removeItem('redirectPath'); + return redirectPath || '/'; +} + +/** + * 执行登录后重定向 + */ +export function redirectAfterLogin(): void { + const redirectPath = getRedirectPath(); + window.location.href = redirectPath; +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..57b4544 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,242 @@ +/** + * Axios HTTP 请求封装 + * @description 封装 Axios,提供统一的请求、响应处理 + */ + +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + AxiosError, +} from 'axios'; +import { Message } from '@arco-design/web-react'; +import { + SUCCESS_CODE, + UNAUTHORIZED_CODE, + FORBIDDEN_CODE, + NOT_FOUND_CODE, + SERVER_ERROR_CODE, +} from '@/constants'; +import type { R } from '@/types/api'; +import { getToken } from './storage'; +import { handleUnauthorized } from './auth'; + +// ==================== 创建 Axios 实例 ==================== + +/** + * 创建 Axios 实例 + */ +const instance: AxiosInstance = axios.create({ + // API 基础地址(从环境变量读取) + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', + // 请求超时时间(从环境变量读取,默认 30 秒) + timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 30000, + // 默认请求头 + headers: { + 'Content-Type': 'application/json', + }, +}); + +// ==================== 请求拦截器 ==================== + +/** + * 请求拦截器 + * 在请求发送前执行 + */ +instance.interceptors.request.use( + (config) => { + // 获取 Token + const token = getToken(); + + // 如果存在 Token,添加到请求头(带 Bearer 前缀) + if (token) { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${token}`; + } + + // 开发环境打印请求日志 + if (import.meta.env.DEV) { + console.log( + `[Request] ${config.method?.toUpperCase()} ${config.url}`, + config.data || config.params + ); + } + + return config; + }, + (error: AxiosError) => { + // 请求错误处理 + console.error('[Request Error]', error); + Message.error('请求配置错误'); + return Promise.reject(error); + } +); + +// ==================== 响应拦截器 ==================== + +/** + * 响应拦截器 + * 在响应返回后执行 + */ +instance.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response; + + // 开发环境打印响应日志 + if (import.meta.env.DEV) { + console.log( + `[Response] ${response.config.method?.toUpperCase()} ${ + response.config.url + }`, + data + ); + } + + // 特殊处理:如果是文件下载(Blob),直接返回完整响应 + if (response.config.responseType === 'blob') { + return response; + } + + // 判断业务响应码(使用宽松相等,兼容字符串和数字) + if (data.code == SUCCESS_CODE) { + // 业务成功,返回完整的响应数据(包含 code, msg, data) + return response; + } else { + // 业务失败,显示错误消息 + const errorMsg = data.msg || '请求失败'; + Message.error(errorMsg); + + // 抛出错误,方便调用方捕获 + return Promise.reject(new Error(errorMsg)); + } + }, + (error: AxiosError) => { + // HTTP 错误处理 + if (error.response) { + const { status, data } = error.response; + + // 开发环境打印错误日志 + if (import.meta.env.DEV) { + console.error('[Response Error]', status, data); + } + + // 根据 HTTP 状态码处理 + switch (status) { + case UNAUTHORIZED_CODE: + // 401 未认证(Token 过期或无效) + Message.error('登录已过期,请重新登录'); + handleUnauthorized(); + break; + + case FORBIDDEN_CODE: + // 403 无权限 + Message.error('没有权限访问该资源'); + break; + + case NOT_FOUND_CODE: + // 404 资源不存在 + Message.error('请求的资源不存在'); + break; + + case SERVER_ERROR_CODE: + // 500 服务器错误 + Message.error('服务器错误,请稍后重试'); + break; + + default: + // 其他错误 + const errorMsg = data?.msg || '请求失败'; + Message.error(errorMsg); + } + } else if (error.request) { + // 请求已发送但未收到响应(网络错误) + console.error('[Network Error]', error.request); + Message.error('网络连接失败,请检查网络'); + } else { + // 请求配置错误 + console.error('[Request Setup Error]', error.message); + Message.error('请求配置错误'); + } + + return Promise.reject(error); + } +); + +// ==================== 封装请求方法 ==================== + +/** + * 封装的请求对象 + */ +const request = { + /** + * GET 请求 + * @param url - 请求 URL + * @param config - 请求配置 + * @returns Promise> + */ + get(url: string, config?: AxiosRequestConfig): Promise> { + return instance.get>(url, config).then((response) => response.data); + }, + + /** + * POST 请求 + * @param url - 请求 URL + * @param data - 请求数据 + * @param config - 请求配置 + * @returns Promise> + */ + post( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return instance + .post>(url, data, config) + .then((response) => response.data); + }, + + /** + * PUT 请求 + * @param url - 请求 URL + * @param data - 请求数据 + * @param config - 请求配置 + * @returns Promise> + */ + put( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return instance + .put>(url, data, config) + .then((response) => response.data); + }, + + /** + * DELETE 请求 + * @param url - 请求 URL + * @param config - 请求配置 + * @returns Promise> + */ + delete(url: string, config?: AxiosRequestConfig): Promise> { + return instance.delete>(url, config).then((response) => response.data); + }, + + /** + * 通用请求方法 + * @param config - 请求配置 + * @returns Promise> + */ + request(config: AxiosRequestConfig): Promise> { + return instance.request>(config).then((response) => response.data); + }, + + /** + * 获取原始 Axios 实例(用于特殊场景) + */ + instance, +}; + +// ==================== 导出 ==================== + +export default request; diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..4fafa4c --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,258 @@ +/** + * 本地存储工具函数 + * @description 封装 localStorage 操作,提供类型安全的存储方法 + */ + +import { + TOKEN_KEY, + USERINFO_KEY, + USER_STATUS_KEY, + USER_ROLE_KEY, + LANG_KEY, + THEME_KEY, +} from '@/constants'; +import type { LocalUserInfo } from '@/types/auth'; + +// ==================== 通用存储方法 ==================== + +/** + * 存储数据到 localStorage + * @param key - 存储键 + * @param value - 存储值(自动 JSON 序列化) + */ +export function setItem(key: string, value: T): void { + try { + const serializedValue = JSON.stringify(value); + localStorage.setItem(key, serializedValue); + } catch (error) { + console.error(`Failed to set item "${key}" to localStorage:`, error); + } +} + +/** + * 从 localStorage 获取数据 + * @param key - 存储键 + * @returns 存储值(自动 JSON 反序列化) + */ +export function getItem(key: string): T | null { + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + return null; + } + return JSON.parse(serializedValue) as T; + } catch (error) { + console.error(`Failed to get item "${key}" from localStorage:`, error); + return null; + } +} + +/** + * 从 localStorage 删除数据 + * @param key - 存储键 + */ +export function removeItem(key: string): void { + try { + localStorage.removeItem(key); + } catch (error) { + console.error(`Failed to remove item "${key}" from localStorage:`, error); + } +} + +/** + * 清空 localStorage 所有数据 + */ +export function clear(): void { + try { + localStorage.clear(); + } catch (error) { + console.error('Failed to clear localStorage:', error); + } +} + +/** + * 检查 localStorage 中是否存在指定键 + * @param key - 存储键 + * @returns 是否存在 + */ +export function hasItem(key: string): boolean { + return localStorage.getItem(key) !== null; +} + +// ==================== Token 相关 ==================== + +/** + * 存储 Token + * @param token - Token 字符串 + */ +export function setToken(token: string): void { + setItem(TOKEN_KEY, token); +} + +/** + * 获取 Token + * @returns Token 字符串 + */ +export function getToken(): string | null { + return getItem(TOKEN_KEY); +} + +/** + * 删除 Token + */ +export function removeToken(): void { + removeItem(TOKEN_KEY); +} + +/** + * 检查是否存在 Token + * @returns 是否已登录 + */ +export function hasToken(): boolean { + return hasItem(TOKEN_KEY); +} + +// ==================== 用户信息相关 ==================== + +/** + * 存储用户信息 + * @param userInfo - 用户信息对象 + */ +export function setUserInfo(userInfo: LocalUserInfo): void { + setItem(USERINFO_KEY, userInfo); +} + +/** + * 获取用户信息 + * @returns 用户信息对象 + */ +export function getUserInfo(): LocalUserInfo | null { + return getItem(USERINFO_KEY); +} + +/** + * 删除用户信息 + */ +export function removeUserInfo(): void { + removeItem(USERINFO_KEY); +} + +/** + * 检查是否存在用户信息 + * @returns 是否存在 + */ +export function hasUserInfo(): boolean { + return hasItem(USERINFO_KEY); +} + +// ==================== 用户状态相关 ==================== + +/** + * 存储用户状态 + * @param status - 用户状态 + */ +export function setUserStatus(status: number): void { + setItem(USER_STATUS_KEY, status); +} + +/** + * 获取用户状态 + * @returns 用户状态 + */ +export function getUserStatus(): number | null { + return getItem(USER_STATUS_KEY); +} + +/** + * 删除用户状态 + */ +export function removeUserStatus(): void { + removeItem(USER_STATUS_KEY); +} + +// ==================== 用户角色相关 ==================== + +/** + * 存储用户角色 + * @param role - 用户角色 + */ +export function setUserRole(role: string): void { + setItem(USER_ROLE_KEY, role); +} + +/** + * 获取用户角色 + * @returns 用户角色 + */ +export function getUserRole(): string | null { + return getItem(USER_ROLE_KEY); +} + +/** + * 删除用户角色 + */ +export function removeUserRole(): void { + removeItem(USER_ROLE_KEY); +} + +// ==================== 语言设置相关 ==================== + +/** + * 存储语言设置 + * @param lang - 语言代码(如:zh-CN, en-US) + */ +export function setLang(lang: string): void { + setItem(LANG_KEY, lang); +} + +/** + * 获取语言设置 + * @returns 语言代码 + */ +export function getLang(): string | null { + return getItem(LANG_KEY); +} + +/** + * 删除语言设置 + */ +export function removeLang(): void { + removeItem(LANG_KEY); +} + +// ==================== 主题设置相关 ==================== + +/** + * 存储主题设置 + * @param theme - 主题名称(如:light, dark) + */ +export function setTheme(theme: string): void { + setItem(THEME_KEY, theme); +} + +/** + * 获取主题设置 + * @returns 主题名称 + */ +export function getTheme(): string | null { + return getItem(THEME_KEY); +} + +/** + * 删除主题设置 + */ +export function removeTheme(): void { + removeItem(THEME_KEY); +} + +// ==================== 清理所有认证信息 ==================== + +/** + * 清理所有认证相关信息(用户登出时调用) + */ +export function clearAuthInfo(): void { + removeToken(); + removeUserInfo(); + removeUserStatus(); + removeUserRole(); +}