217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
/**
|
||
* 权限验证相关工具函数
|
||
*/
|
||
|
||
import type { RouteLocationNormalized } from 'vue-router'
|
||
import type { UserInfo } from '@/types/api'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
|
||
/**
|
||
* 不需要登录的路由白名单
|
||
*/
|
||
export const LOGIN_WHITE_LIST = [
|
||
'/auth/login',
|
||
'/auth/register',
|
||
'/auth/forget-password',
|
||
'/exception/403',
|
||
'/exception/404',
|
||
'/exception/500'
|
||
]
|
||
|
||
/**
|
||
* 检查路由是否在白名单中
|
||
*/
|
||
export const isInWhiteList = (path: string): boolean => {
|
||
return LOGIN_WHITE_LIST.some((whitePath) => {
|
||
if (whitePath.endsWith('*')) {
|
||
// 支持通配符匹配
|
||
const prefix = whitePath.slice(0, -1)
|
||
return path.startsWith(prefix)
|
||
}
|
||
return path === whitePath
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否有权限访问路由
|
||
* 根据文档要求:菜单访问基于 URL,按钮访问基于权限标识
|
||
*/
|
||
export const hasRoutePermission = (
|
||
route: RouteLocationNormalized,
|
||
userInfo: Partial<UserInfo>
|
||
): boolean => {
|
||
const { roles = [], permissions = [] } = userInfo
|
||
|
||
// 如果是超级管理员,直接通过
|
||
if (userInfo.user_type === 1) {
|
||
return true
|
||
}
|
||
|
||
// 默认始终可访问的路由(不需要菜单权限)
|
||
const defaultAllowedPaths = ['/dashboard']
|
||
const isDefaultRoute = defaultAllowedPaths.some(
|
||
(allowedPath) => route.path === allowedPath || route.path.startsWith(allowedPath + '/')
|
||
)
|
||
|
||
// 检查菜单访问权限(基于 URL)
|
||
const userStore = useUserStore()
|
||
const userMenus = userStore.menus || []
|
||
|
||
// 如果是默认路由,跳过菜单权限检查
|
||
if (!isDefaultRoute && userMenus.length > 0) {
|
||
const hasMenuAccess = checkMenuAccess(route.path, userMenus)
|
||
if (!hasMenuAccess) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 如果路由没有设置额外的权限要求,直接通过
|
||
if (!route.meta?.roles && !route.meta?.permissions) {
|
||
return true
|
||
}
|
||
|
||
// 检查角色权限
|
||
if (route.meta.roles) {
|
||
const routeRoles = route.meta.roles as string[]
|
||
const hasRole = routeRoles.some((role) => roles.includes(role as any))
|
||
if (!hasRole) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查操作权限
|
||
if (route.meta.permissions) {
|
||
const routePermissions = route.meta.permissions as string[]
|
||
const hasPermission = routePermissions.some((permission) => {
|
||
if (permissions.includes('*:*:*')) {
|
||
return true
|
||
}
|
||
return permissions.some((userPermission) => {
|
||
if (userPermission.endsWith('*')) {
|
||
const prefix = userPermission.slice(0, -1)
|
||
return permission.startsWith(prefix)
|
||
}
|
||
return userPermission === permission
|
||
})
|
||
})
|
||
if (!hasPermission) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 递归检查菜单访问权限(基于 URL)
|
||
*/
|
||
function checkMenuAccess(path: string, menus: any[]): boolean {
|
||
for (const menu of menus) {
|
||
// 检查当前菜单的 URL 是否匹配(支持动态参数)
|
||
if (menu.url && matchMenuPath(path, menu.url)) {
|
||
console.log(`[菜单权限检查] 路径 ${path} 匹配菜单 ${menu.url}`)
|
||
return true
|
||
}
|
||
|
||
// 递归检查子菜单
|
||
if (menu.children && menu.children.length > 0) {
|
||
if (checkMenuAccess(path, menu.children)) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
console.log(`[菜单权限检查] 路径 ${path} 未找到匹配的菜单`)
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 匹配菜单路径,支持动态参数
|
||
* @param actualPath 实际访问路径,如 /account-management/enterprise-customer/customer-accounts/3000
|
||
* @param menuPath 菜单定义路径,如 /account-management/enterprise-customer/customer-accounts/:id
|
||
*/
|
||
function matchMenuPath(actualPath: string, menuPath: string): boolean {
|
||
// 移除查询参数
|
||
const cleanActualPath = actualPath.split('?')[0]
|
||
const cleanMenuPath = menuPath.split('?')[0]
|
||
|
||
// 如果完全匹配,直接返回
|
||
if (cleanActualPath === cleanMenuPath) {
|
||
return true
|
||
}
|
||
|
||
// 将路径分割成段
|
||
const actualSegments = cleanActualPath.split('/').filter(Boolean)
|
||
const menuSegments = cleanMenuPath.split('/').filter(Boolean)
|
||
|
||
// 段数必须相同
|
||
if (actualSegments.length !== menuSegments.length) {
|
||
return false
|
||
}
|
||
|
||
// 逐段比较
|
||
for (let i = 0; i < menuSegments.length; i++) {
|
||
const menuSegment = menuSegments[i]
|
||
const actualSegment = actualSegments[i]
|
||
|
||
// 如果是动态参数(以 : 开头),跳过比较
|
||
if (menuSegment.startsWith(':')) {
|
||
continue
|
||
}
|
||
|
||
// 如果不是动态参数,必须完全匹配
|
||
if (menuSegment !== actualSegment) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 检查 Token 是否有效
|
||
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
||
*/
|
||
export const isTokenValid = (token: string): boolean => {
|
||
if (!token) return false
|
||
|
||
// Mock Token 格式: mock_token_{key}_{timestamp}
|
||
if (token.startsWith('mock_token_')) {
|
||
// Mock Token 永不过期(开发环境)
|
||
return true
|
||
}
|
||
|
||
// 真实 Token 可以在这里添加 JWT 解析和过期检查
|
||
// 例如:
|
||
// try {
|
||
// const decoded = jwt_decode(token)
|
||
// const isExpired = decoded.exp * 1000 < Date.now()
|
||
// return !isExpired
|
||
// } catch {
|
||
// return false
|
||
// }
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 获取重定向路径
|
||
* 登录后跳转到之前访问的页面
|
||
*/
|
||
export const getRedirectPath = (route: RouteLocationNormalized): string => {
|
||
const redirect = route.query.redirect as string
|
||
if (redirect && !isInWhiteList(redirect)) {
|
||
return redirect
|
||
}
|
||
return '/'
|
||
}
|
||
|
||
/**
|
||
* 构建登录重定向 URL
|
||
*/
|
||
export const buildLoginRedirect = (currentPath: string): string => {
|
||
if (isInWhiteList(currentPath)) {
|
||
return '/auth/login'
|
||
}
|
||
return `/auth/login?redirect=${encodeURIComponent(currentPath)}`
|
||
}
|