Robot Admin 认证与授权详解
🔐 全方位安全体系架构解析
深入理解基于令牌的身份验证、动态权限控制和路由保护机制,构建安全可靠的企业级应用
Robot Admin 实现了一个全面的基于令牌的身份验证和授权系统。本指南将详细介绍用户认证流程、动态路由保护和权限管理如何协同工作,确保应用程序的安全性和用户体验。

🎯 系统设计理念
认证与授权系统基于以下核心原则构建:
原则 | 说明 | 实现价值 |
---|---|---|
安全优先 | 多层防护,全链路安全验证 | 保障数据安全,防止越权访问 |
用户体验 | 无感知认证,优雅降级处理 | 提升用户使用体验 |
动态权限 | 基于角色的动态路由生成 | 灵活适应不同用户权限 |
可扩展性 | 支持多种认证方式扩展 | 适应不同业务场景需求 |
💡 架构优势
系统采用 JWT 令牌管理,结合 Pinia 状态管理和本地存储,实现了完整的认证生命周期管理,确保用户会话的安全性和持久性。
🔑 身份验证系统
令牌管理机制
身份验证依赖于存储在应用程序状态和本地存储中的 JWT 令牌:
typescript
// 🔐 令牌存储和检索
export const s_userStore = defineStore("user", {
state: () => ({
token: "",
userInfo: {},
timestamp: 0,
}),
actions: {
// 💾 设置令牌
setToken(token: string) {
this.token = token;
this.timestamp = Date.now();
// 同步到本地存储
setItem(TOKEN, token);
setItem(TIMESTAMP, this.timestamp.toString());
},
// 🔍 获取令牌
getToken(): string {
return this.token || getItem(TOKEN) || "";
},
// 🕒 检查令牌有效性
isTokenValid(): boolean {
const token = this.getToken();
if (!token) return false;
return !this.isTokenExpired();
},
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typescript
// ⏰ 令牌超时检查
export const d_isCheckTimeout = (): boolean => {
const currentTime = Date.now();
const timeStamp = d_getTimeStamp();
// 🔍 检查是否超过配置的超时时间
return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE;
};
// 📅 获取时间戳
export const d_getTimeStamp = (): number => {
return Number(getItem(TIMESTAMP)) || 0;
};
// 🔄 刷新令牌时间戳
export const d_refreshTimestamp = (): void => {
const currentTime = Date.now();
setItem(TIMESTAMP, currentTime.toString());
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
登录流程详解
当用户成功登录时,系统执行以下步骤:
- 🔐 验证凭据 - 向后端发送用户凭据
- 💾 存储令牌 - 将 JWT 令牌存储在状态和本地存储中
- ⏰ 记录时间戳 - 跟踪令牌创建时间用于过期检查
- 🚀 获取用户信息 - 获取用户基本信息和权限
- 🔄 重定向导航 - 重定向到主页或目标页面
typescript
// 🚀 登录处理流程
async login(credentials: LoginCredentials) {
try {
// 1️⃣ 发送登录请求
const response = await authApi.login(credentials)
if (response.code === 200) {
// 2️⃣ 存储令牌和用户信息
this.setToken(response.data.token)
this.setUserInfo(response.data.userInfo)
// 3️⃣ 初始化权限
const permissionStore = s_permissionStore()
await permissionStore.getAuthMenuList()
// 4️⃣ 重定向到目标页面
const redirectPath = getItem(REDIRECT_PATH) || '/dashboard'
removeItem(REDIRECT_PATH)
router.push(redirectPath)
// 5️⃣ 显示成功消息
ElMessage.success('登录成功')
}
} catch (error) {
// ❌ 登录失败处理
console.error('登录失败:', error)
ElMessage.error('登录失败,请检查用户名和密码')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
🛡️ API 请求拦截
请求拦截器
所有 API 请求都通过 Axios 拦截器系统包含身份验证令牌:
typescript
// 🔧 请求拦截器配置
service.interceptors.request.use(
(config) => {
const userStore = s_userStore();
const { token, logout } = userStore;
if (token) {
// ⏰ 检查令牌是否过期
if (d_isCheckTimeout()) {
logout();
return Promise.reject(new Error("令牌已过期,请重新登录"));
}
// 🔑 添加授权头
config.headers.Authorization = `Bearer ${token}`;
// 🔄 刷新时间戳
d_refreshTimestamp();
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typescript
// 📨 响应拦截器配置
service.interceptors.response.use(
(response) => {
const { code, message } = response.data;
// ✅ 成功响应处理
if (code === 200) {
return response.data;
}
// ⚠️ 业务错误处理
ElMessage.error(message || "请求失败");
return Promise.reject(new Error(message));
},
(error) => {
const { response } = error;
if (response) {
switch (response.status) {
case 401:
// 🔐 未授权,清除令牌并重定向到登录
const userStore = s_userStore();
userStore.logout();
ElMessage.error("登录已过期,请重新登录");
break;
case 403:
// 🚫 权限不足
ElMessage.error("权限不足,无法访问");
break;
case 500:
// 💥 服务器错误
ElMessage.error("服务器内部错误");
break;
default:
ElMessage.error("网络请求失败");
}
}
return Promise.reject(error);
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
🎛️ 权限管理系统
权限存储结构
权限存储管理用户的菜单权限和按钮权限:
typescript
// 🗂️ 权限存储定义
export const s_permissionStore = defineStore("permission", {
state: () => ({
// 📋 授权菜单列表
authMenuList: [] as DynamicRoute[],
// 🔘 授权按钮列表
authButtonList: {} as Record<string, boolean>,
// 🔄 路由是否已初始化
routesLoaded: false,
}),
getters: {
// 🌐 扁平化菜单列表
flatMenuList: (state) => {
return flattenMenuList(state.authMenuList);
},
// 🔍 菜单权限检查
hasMenuPermission: (state) => {
return (path: string) => {
return state.flatMenuList.some((menu) => menu.path === path);
};
},
// 🔘 按钮权限检查
hasButtonPermission: (state) => {
return (key: string) => {
return !!state.authButtonList[key];
};
},
},
actions: {
// 📥 获取权限数据
async getAuthMenuList() {
try {
const { data } = await getAuthMenuListApi();
this.authMenuList = data.menuList || [];
this.authButtonList = data.buttonList || {};
// 🔄 初始化动态路由
await this.initDynamicRouter();
this.routesLoaded = true;
} catch (error) {
console.error("获取权限数据失败:", error);
}
},
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
动态路由生成
系统根据用户权限动态生成路由:

typescript
// 🔄 动态路由初始化
async initDynamicRouter() {
try {
// 1️⃣ 处理路由数据
const dynamicRoutes = await this.processDynamicRoutes(this.authMenuList)
// 2️⃣ 注册路由
dynamicRoutes.forEach(route => {
router.addRoute('Layout', route)
})
// 3️⃣ 添加404路由
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error-page/404.vue')
})
console.log('✅ 动态路由初始化完成')
} catch (error) {
console.error('❌ 动态路由初始化失败:', error)
}
}
// 🔧 处理动态路由
async processDynamicRoutes(menuList: any[]): Promise<RouteRecordRaw[]> {
const routes: RouteRecordRaw[] = []
for (const menu of menuList) {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.name,
component: await this.resolveComponent(menu.component),
meta: {
title: menu.title,
icon: menu.icon,
requiresAuth: true,
permissions: menu.permissions || []
}
}
// 🔄 处理子路由
if (menu.children && menu.children.length > 0) {
route.children = await this.processDynamicRoutes(menu.children)
}
routes.push(route)
}
return routes
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
typescript
// 🧩 组件解析器
async resolveComponent(componentPath: string) {
if (!componentPath) {
return () => import('@/views/error-page/404.vue')
}
try {
// 🔍 标准化组件路径
const normalizedPath = componentPath.startsWith('@/')
? componentPath
: `@/views/${componentPath}.vue`
// 📦 动态导入组件
const component = await import(/* @vite-ignore */ normalizedPath)
return component.default || component
} catch (error) {
console.warn(`组件加载失败: ${componentPath}`, error)
return () => import('@/views/error-page/404.vue')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🛣️ 路由保护机制
导航守卫
导航守卫确保用户只能访问其被授权的路由:
typescript
// 🔐 路由保护守卫
router.beforeEach(async (to, from, next) => {
// 🚀 启动加载指示器
NProgress.start();
const userStore = s_userStore();
const permissionStore = s_permissionStore();
const { token } = userStore;
const { routesLoaded } = permissionStore;
// 📝 设置页面标题
document.title = getPageTitle(to.meta.title);
// 1️⃣ 检查用户认证状态
if (!token) {
if (WHITE_LIST.includes(to.path)) {
// ✅ 白名单路由直接通过
next();
} else {
// 🔐 未认证用户重定向到登录页
setItem(REDIRECT_PATH, to.fullPath);
next("/login");
}
return;
}
// 2️⃣ 已认证用户访问登录页,重定向到首页
if (to.path === "/login") {
next("/dashboard");
return;
}
// 3️⃣ 检查动态路由是否已加载
if (!routesLoaded) {
try {
// 🔄 初始化权限和路由
await permissionStore.getAuthMenuList();
// 🔄 重新导航到目标路由
next({ ...to, replace: true });
} catch (error) {
// ❌ 初始化失败,登出用户
console.error("权限初始化失败:", error);
await userStore.logout();
next("/login");
}
return;
}
// 4️⃣ 检查路由权限
if (to.meta.requiresAuth && !permissionStore.hasMenuPermission(to.path)) {
// 🚫 无权限访问
ElMessage.error("无权限访问该页面");
next("/403");
return;
}
// ✅ 通过所有检查
next();
});
// 🏁 路由后置守卫
router.afterEach(() => {
NProgress.done();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
路由元信息
路由元信息用于定义访问权限和页面属性:
typescript
// 📋 路由元信息类型定义
interface RouteMeta {
/** 页面标题 */
title?: string;
/** 图标 */
icon?: string;
/** 是否需要认证 */
requiresAuth?: boolean;
/** 所需权限列表 */
permissions?: string[];
/** 是否隐藏在菜单中 */
hidden?: boolean;
/** 是否缓存页面 */
keepAlive?: boolean;
/** 面包屑配置 */
breadcrumb?: {
hide?: boolean;
showHome?: boolean;
};
}
// 🎯 权限路由示例
const protectedRoutes: RouteRecordRaw[] = [
{
path: "/users",
name: "UserManagement",
component: () => import("@/views/users/index.vue"),
meta: {
title: "用户管理",
icon: "user",
requiresAuth: true,
permissions: ["user:view", "user:list"],
},
},
{
path: "/users/create",
name: "CreateUser",
component: () => import("@/views/users/create.vue"),
meta: {
title: "新建用户",
requiresAuth: true,
permissions: ["user:create"],
breadcrumb: {
showHome: true,
},
},
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
🚪 登出流程
登出流程确保彻底清理应用程序状态:
typescript
// 🚪 完整登出流程
async logout(showMessage = true) {
try {
// 1️⃣ 调用后端登出接口(可选)
try {
await logoutApi()
} catch (error) {
console.warn('后端登出接口调用失败:', error)
}
// 2️⃣ 清除用户状态
this.token = ''
this.userInfo = {}
this.timestamp = 0
// 3️⃣ 重置页面标题
document.title = import.meta.env.VITE_APP_TITLE
// 4️⃣ 清除本地存储
removeAllItem()
// 5️⃣ 重置其他存储
const appStore = s_appStore()
const permissionStore = s_permissionStore()
appStore.$reset()
permissionStore.$reset()
// 6️⃣ 清理动态路由
const { clearExistingRoutes } = await import('@/router/dynamicRouter')
clearExistingRoutes()
// 7️⃣ 导航到登录页
router.replace('/login')
// 8️⃣ 显示提示消息
if (showMessage) {
ElMessage.success('登出成功')
}
} catch (error) {
console.error('登出过程中发生错误:', error)
// 🔄 强制清理(即使出错也要确保清理完成)
this.forceLogout()
}
}
// 💥 强制登出(用于异常情况)
forceLogout() {
// 清除所有本地数据
localStorage.clear()
sessionStorage.clear()
// 重新加载页面
window.location.reload()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
🎨 组件权限控制
权限指令
自定义指令用于控制元素的显示权限:
typescript
// 🔐 权限指令定义
export const vPermission = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const permissionStore = s_permissionStore();
if (value) {
const hasPermission = Array.isArray(value)
? value.some((permission) =>
permissionStore.hasButtonPermission(permission)
)
: permissionStore.hasButtonPermission(value);
if (!hasPermission) {
// 🚫 无权限时移除元素或隐藏
el.style.display = "none";
// 或者直接移除:el.parentNode?.removeChild(el)
}
}
},
updated(el: HTMLElement, binding: DirectiveBinding) {
// 🔄 权限变化时重新检查
this.mounted(el, binding);
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vue
<template>
<div class="user-management">
<!-- 🔘 基于权限显示按钮 -->
<n-button v-permission="'user:create'" type="primary" @click="createUser">
新建用户
</n-button>
<!-- 🔘 多权限控制(满足其一即可) -->
<n-button
v-permission="['user:edit', 'user:update']"
type="default"
@click="editUser"
>
编辑用户
</n-button>
<!-- 🔘 使用组合式函数检查权限 -->
<n-button v-if="hasDeletePermission" type="error" @click="deleteUser">
删除用户
</n-button>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { s_permissionStore } from "@/stores/permission";
const permissionStore = s_permissionStore();
// 🔍 使用组合式函数检查权限
const hasDeletePermission = computed(() =>
permissionStore.hasButtonPermission("user:delete")
);
const createUser = () => {
// 创建用户逻辑
};
const editUser = () => {
// 编辑用户逻辑
};
const deleteUser = () => {
// 删除用户逻辑
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
权限组合式函数
提供便捷的权限检查方法:
typescript
// 🎯 权限检查组合式函数
export function usePermission() {
const permissionStore = s_permissionStore();
// 🔍 检查菜单权限
const hasMenuPermission = (path: string): boolean => {
return permissionStore.hasMenuPermission(path);
};
// 🔘 检查按钮权限
const hasButtonPermission = (key: string): boolean => {
return permissionStore.hasButtonPermission(key);
};
// 🎯 检查多个权限(AND操作)
const hasAllPermissions = (permissions: string[]): boolean => {
return permissions.every((permission) =>
permissionStore.hasButtonPermission(permission)
);
};
// 🎯 检查多个权限(OR操作)
const hasAnyPermission = (permissions: string[]): boolean => {
return permissions.some((permission) =>
permissionStore.hasButtonPermission(permission)
);
};
// 🔄 权限响应式状态
const permissions = computed(() => permissionStore.authButtonList);
return {
hasMenuPermission,
hasButtonPermission,
hasAllPermissions,
hasAnyPermission,
permissions,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
📋 最佳实践指南
安全建议
建议 | 说明 | 重要性 |
---|---|---|
后端验证 | 始终在后端验证权限,不要仅依赖前端隐藏 | 🔴 关键 |
令牌安全 | 使用 HTTPS 传输,合理设置过期时间 | 🔴 关键 |
错误处理 | 优雅处理认证失败和权限不足 | 🟡 重要 |
日志记录 | 记录认证和授权相关的关键操作 | 🟡 重要 |
开发指南
typescript
// ✅ 推荐的权限检查方式
const checkUserPermission = () => {
const { hasButtonPermission } = usePermission();
// 方式1:使用组合式函数
if (hasButtonPermission("user:delete")) {
// 执行删除操作
}
// 方式2:在模板中使用指令
// <button v-permission="'user:delete'">删除</button>
// 方式3:在计算属性中使用
const canDelete = computed(() => hasButtonPermission("user:delete"));
};
// ❌ 避免的做法
const badPractice = () => {
// 不要硬编码权限检查
if (userRole === "admin") {
// 这样做不够灵活
}
// 不要仅依赖前端权限控制
// 后端API也必须进行权限验证
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
测试建议
- 🔧 不同权限集测试 - 确保 UI 适应不同用户角色
- ⏰ 令牌过期测试 - 验证令牌过期时的处理逻辑
- 🚫 边界情况测试 - 测试网络异常、API 错误等情况
- 🔐 安全漏洞测试 - 进行基本的安全漏洞扫描
🎯 开发建议
遵循这些最佳实践,您将创建既安全又用户友好的功能,并正确集成到 Robot Admin 的认证授权系统中。记住,前端权限控制主要用于用户体验优化,真正的安全保障必须在后端实现。