牛商网做的网站如何仙桃网站设计公司
牛商网做的网站如何,仙桃网站设计公司,个人网站备案 网站名称,网站推广策划的思路包括哪些内容1. 开篇#xff1a;为什么我们需要一个健壮的认证与权限系统#xff1f;
如果你正在用 Vue3 开发一个后台管理系统#xff0c;或者任何需要区分用户角色的应用#xff0c;那你肯定绕不开两个核心问题#xff1a;用户怎么安全地登录和保持登录状态#xff1f; 以及 不同角…1. 开篇为什么我们需要一个健壮的认证与权限系统如果你正在用 Vue3 开发一个后台管理系统或者任何需要区分用户角色的应用那你肯定绕不开两个核心问题用户怎么安全地登录和保持登录状态以及不同角色的用户能看到和操作哪些不同的内容这两个问题就是我们常说的“认证”和“授权”。我经历过不少项目早期为了赶进度常常把用户登录后的 Token 直接往localStorage里一扔权限判断就靠一堆if-else写在各个组件里。结果呢代码又臭又长维护起来像在走迷宫Token 过期了用户被莫名其妙踢出按钮权限漏判一个就可能出大问题。后来我痛定思痛摸索出了一套用Vue3 Pinia Axios的组合拳把整个流程梳理得清清楚楚。这套方案不仅逻辑清晰而且复用性极高基本上在新项目里复制粘贴改改接口地址就能跑起来。简单来说我们要实现的目标是用户登录安全地获取并管理 Token包括短期的access_token和用于续期的refresh_token。状态管理用 Pinia 集中、持久化地存储用户信息和权限数据全局任何地方都能轻松访问。请求拦截通过 Axios 拦截器自动为每个请求带上 Token并在 Token 过期时静默地刷新它用户完全无感知。权限控制根据用户权限动态控制前端路由的访问和页面内按钮、菜单的显示与隐藏。听起来有点复杂别担心我会把每一步都掰开揉碎用最直白的代码和例子讲给你听。咱们不搞理论直接上手实战。2. 搭建基石用 Pinia 创建全局用户状态仓库Pinia 是 Vue 官方推荐的状态管理库比 Vuex 更简单、更符合 Vue3 的组合式 API 思想。我们的第一步就是用它来创建一个管理用户所有信息的“中央仓库”。2.1 安装与基础配置首先确保你的项目已经创建好。然后我们安装必要的依赖npm install pinia pinia-plugin-persistedstate # 或者使用 yarn yarn add pinia pinia-plugin-persistedstate # 或者使用 pnpm pnpm add pinia pinia-plugin-persistedstate这里多装了一个pinia-plugin-persistedstate插件它的作用超级实用让 Pinia 仓库的数据可以持久化保存到localStorage或sessionStorage。这样即使页面刷新用户登录状态也不会丢失。接下来我们在src/stores目录下创建仓库。我喜欢用一个index.js文件作为入口统一导出所有仓库。// src/stores/index.js import { createPinia } from pinia import persist from pinia-plugin-persistedstate // 导入持久化插件 // 创建 pinia 实例 const pinia createPinia() // 使用持久化插件 pinia.use(persist) export default pinia // 这里我们之后会导出具体的 store export * from ./user然后在main.js中全局启用这个 Pinia 实例// src/main.js import { createApp } from vue import App from ./App.vue import pinia from ./stores // 导入我们刚刚创建的 pinia 实例 const app createApp(App) app.use(pinia) // 使用 pinia app.mount(#app)2.2 创建核心的用户状态仓库现在我们来创建最核心的user仓库。这个仓库将负责管理用户的 Token、个人信息和权限。// src/stores/user.js import { ref, computed } from vue import { defineStore } from pinia import axios from axios // 假设我们有一个包含所有权限定义的文件或者从接口获取 // 这里为了演示先定义一个本地权限列表。实际项目中这个列表很可能来自后端接口。 const allPermissions [ { id: 1, name: 查看仪表盘 }, { id: 2, name: 用户管理 }, { id: 3, name: 添加用户 }, { id: 4, name: 编辑用户 }, { id: 5, name: 文章管理 }, // ... 更多权限 ] export const useUserStore defineStore(user, () { // 1. 定义状态 const userInfo ref({}) // 用户详细信息如姓名、头像、角色等 const accessToken ref() // 访问令牌 const refreshToken ref() // 刷新令牌 // 2. 定义计算属性派生状态 // 例如判断用户是否登录简单通过 token 是否存在 const isLoggedIn computed(() !!accessToken.value) // 3. 定义 Actions修改状态的方法 // 登录成功后的设置方法 const setToken (access, refresh) { accessToken.value access refreshToken.value refresh // 注意pinia-plugin-persistedstate 会帮我们自动持久化到 storage // 但如果你需要立即存入 sessionStorage 供拦截器使用也可以在这里操作。 sessionStorage.setItem(access_token, access) sessionStorage.setItem(refresh_token, refresh) } // 清除用户信息退出登录时调用 const clearUserInfo () { userInfo.value {} accessToken.value refreshToken.value sessionStorage.removeItem(access_token) sessionStorage.removeItem(refresh_token) } // 异步获取用户详细信息 const fetchUserInfo async () { try { // 这里使用 axios 发起请求拦截器会自动添加 Token const response await axios.get(/api/user/info) if (response.data.code 200) { // 假设成功码为 200 userInfo.value response.data.data } else { throw new Error(获取用户信息失败) } } catch (error) { console.error(获取用户信息出错:, error) // 可以根据错误类型进行更细致的处理比如 token 无效则跳转登录 } } // **核心方法判断当前用户是否拥有某个权限** const hasPermission (permissionName) { // 假设 userInfo.value.permissions 是一个权限ID数组如 [1, 3, 5] const userPermissionIds userInfo.value.permissions || [] // 在总权限列表中根据权限名找到对应的权限ID const targetPermission allPermissions.find(p p.name permissionName) if (!targetPermission) { console.warn(权限名 ${permissionName} 未在全局权限列表中找到) return false } // 检查用户的权限ID列表是否包含目标权限ID return userPermissionIds.includes(targetPermission.id) } // 4. 返回所有需要暴露的状态和方法 return { userInfo, accessToken, refreshToken, isLoggedIn, setToken, clearUserInfo, fetchUserInfo, hasPermission, } }, { // Pinia 持久化插件的配置 persist: { key: user-store, // 存储的 key storage: sessionStorage, // 默认是 localStorage这里改用 sessionStorage paths: [accessToken, refreshToken, userInfo], // 只持久化这几项 } })这个user仓库就是我们整个认证系统的大脑。它封装了所有和用户相关的数据与操作。hasPermission方法是权限控制的灵魂我们后面会频繁用到它。3. 构建血管网络配置 Axios 拦截器实现自动化 Token 管理仓库有了数据怎么流动起来这就需要 Axios 拦截器出场了。它就像项目的血管网络负责所有 HTTP 请求的“输送”我们可以在请求发出前和响应返回后插入自己的逻辑。3.1 创建 Axios 实例并设置请求拦截器我们单独创建一个request.js文件来配置 Axios这样更清晰。// src/utils/request.js import axios from axios import { useUserStore } from /stores/user import router from /router // 需要引入路由实例 // 创建一个独立的 axios 实例避免污染全局 axios const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, // 从环境变量读取基础URL timeout: 10000, // 请求超时时间 }) // **请求拦截器 (在请求发出之前干活)** service.interceptors.request.use( (config) { // 从 store 或 sessionStorage 中获取 token const userStore useUserStore() const token userStore.accessToken || sessionStorage.getItem(access_token) // 如果 token 存在将其添加到请求头中 if (token) { config.headers.Authorization Bearer ${token} } // 可以在这里统一添加其他 headers如 Content-Type // config.headers[Content-Type] application/json; return config }, (error) { // 对请求错误做些什么比如网络错误 return Promise.reject(error) } )请求拦截器的作用很简单在每一次请求发出前自动从我们的 Pinia 仓库里拿到最新的access_token然后把它塞到请求的Authorization头里。这样后端接口就能识别出是谁在请求了。3.2 实现响应拦截器与 Token 自动刷新这是整个流程中最精妙也最容易踩坑的部分处理 Token 过期并自动刷新。很多应用在 Token 过期后直接跳回登录页用户体验很糟糕。我们要实现的是当access_token过期用refresh_token静默获取新的access_token然后重试刚才失败的请求用户完全感知不到。// 接上面的 src/utils/request.js // 用于标记是否正在刷新 token防止并发请求导致多次刷新 let isRefreshing false // 存储等待 token 刷新完成的请求队列 let requestsQueue [] // **响应拦截器 (在响应返回之后干活)** service.interceptors.response.use( (response) { // 对响应数据做点什么状态码在 2xx 范围内 // 这里可以统一处理后端返回的业务代码例如 const res response.data if (res.code 200) { return res.data // 直接返回核心数据方便组件使用 } else { // 业务逻辑错误例如参数错误、权限不足等非401 // 可以统一弹窗提示 console.error(业务错误: ${res.message}) return Promise.reject(new Error(res.message || Error)) } }, async (error) { // 对响应错误做点什么状态码超出 2xx 范围 const originalRequest error.config const userStore useUserStore() // **核心判断是否为 401 未授权错误且不是刷新 token 的请求本身** if (error.response?.status 401 !originalRequest._retry) { // 标记这个请求已经重试过防止死循环 originalRequest._retry true // 如果当前没有在刷新 token则开始刷新流程 if (!isRefreshing) { isRefreshing true const currentRefreshToken userStore.refreshToken || sessionStorage.getItem(refresh_token) if (!currentRefreshToken) { // 连 refresh_token 都没有直接跳转登录 userStore.clearUserInfo() router.push(/login) return Promise.reject(error) } try { // 调用刷新 token 的接口 const refreshResponse await axios.post(/api/auth/refresh, { refresh_token: currentRefreshToken }) const newAccessToken refreshResponse.data.data.access_token const newRefreshToken refreshResponse.data.data.refresh_token // 有时后端会返回新的 refresh_token // 1. 更新 store 和 storage userStore.setToken(newAccessToken, newRefreshToken || currentRefreshToken) // 2. 更新当前失败请求的 Authorization 头 originalRequest.headers.Authorization Bearer ${newAccessToken} // 3. 执行等待队列中的所有请求用新的 token requestsQueue.forEach(callback callback(newAccessToken)) requestsQueue [] // 4. 重新发起最开始失败的那个请求 return service(originalRequest) } catch (refreshError) { // 刷新 token 也失败了比如 refresh_token 也过期了 console.error(刷新 Token 失败:, refreshError) // 清除所有用户信息跳转到登录页 userStore.clearUserInfo() router.push(/login) // 清空请求队列 requestsQueue [] return Promise.reject(refreshError) } finally { isRefreshing false } } else { // 如果正在刷新 token将当前请求加入队列等待刷新完成后重试 return new Promise((resolve) { requestsQueue.push((newToken) { originalRequest.headers.Authorization Bearer ${newToken} resolve(service(originalRequest)) }) }) } } // 处理其他非 401 的错误例如 404, 500 等 // 可以在这里统一处理网络错误或服务器错误提示 const message error.response?.data?.message || error.message || 网络错误 console.error(请求错误 [${error.response?.status}]:, message) // 可以引入 UI 组件库的消息提示例如 ElMessage.error(message) return Promise.reject(error) } ) export default service这段代码是保障用户体验的关键。它的逻辑就像一个智能调度中心当第一个请求因 Token 过期失败时它会锁定刷新状态发起刷新请求在此期间其他所有失败的请求都会被暂存到队列里等拿到新 Token 后不仅重试了第一个请求还把队列里积压的请求全部用新 Token 重发一遍。整个过程对用户来说是透明的。最后在main.js中全局引入这个配置好的service或者在你需要的地方直接导入使用。// src/main.js (补充) import request from ./utils/request // 可以挂载到全局属性但更推荐在需要的地方直接 import app.config.globalProperties.$http request4. 登录流程串联从表单提交到状态更新有了仓库和请求工具登录流程就变得非常清晰了。我们来看一个登录组件的具体实现。!-- src/views/Login.vue -- template div classlogin-container el-form :modelloginForm :rulesrules refformRef el-form-item propusername el-input v-modelloginForm.username placeholder请输入用户名 / /el-form-item el-form-item proppassword el-input v-modelloginForm.password typepassword placeholder请输入密码 / /el-form-item el-form-item el-button typeprimary :loadingloading clickhandleLogin登录/el-button /el-form-item /el-form /div /template script setup import { ref, reactive } from vue import { useRouter } from vue-router import { ElMessage } from element-plus // 示例用 Element Plus 的提示组件 import { useUserStore } from /stores/user import request from /utils/request // 导入我们封装好的 axios 实例 const router useRouter() const userStore useUserStore() const formRef ref() const loading ref(false) const loginForm reactive({ username: , password: }) const rules { username: [{ required: true, message: 请输入用户名, trigger: blur }], password: [{ required: true, message: 请输入密码, trigger: blur }] } const handleLogin async () { // 1. 表单验证 await formRef.value.validate() loading.value true try { // 2. 调用登录接口 const response await request.post(/api/auth/login, loginForm) // 假设返回数据结构为 { code: 200, data: { access_token: xxx, refresh_token: yyy } } const { access_token, refresh_token } response // 3. 将 Token 存入 store (store 的 action 会处理持久化) userStore.setToken(access_token, refresh_token) // 4. 获取用户详细信息权限、角色、姓名等 await userStore.fetchUserInfo() // 5. 登录成功提示并跳转 ElMessage.success(登录成功) router.push(/dashboard) // 跳转到首页或仪表盘 } catch (error) { // 登录失败处理用户名密码错误、网络问题等 ElMessage.error(error.message || 登录失败请检查用户名和密码) console.error(登录失败:, error) } finally { loading.value false } } /script这个登录组件做了几件关键事验证输入、发送请求、存储 Token、获取用户详情。当userStore.fetchUserInfo()执行成功后我们的 Pinia 仓库里就充满了当前用户的所有信息为接下来的权限控制做好了准备。5. 动态权限控制实战路由守卫与按钮级权限权限控制分为两个层面路由级用户能访问哪些页面和组件/按钮级用户在页面上能进行哪些操作。5.1 路由守卫控制页面访问权限我们使用 Vue Router 的导航守卫在用户跳转到任何页面之前进行检查。// src/router/index.js 或 src/router/permission.js import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user // 假设这是你的路由表其中一些路由需要特定权限 const routes [ { path: /login, name: Login, component: () import(/views/Login.vue), meta: { requiresAuth: false } // 明确标记登录页不需要认证 }, { path: /dashboard, name: Dashboard, component: () import(/views/Dashboard.vue), meta: { requiresAuth: true } // 需要登录才能访问 }, { path: /user-management, name: UserManagement, component: () import(/views/user/Management.vue), meta: { requiresAuth: true, permission: 用户管理 // 需要“用户管理”权限才能访问 } }, { path: /admin, name: Admin, component: () import(/views/Admin.vue), meta: { requiresAuth: true, permission: 系统管理员 // 需要“系统管理员”角色或权限 } }, // ... 其他路由 ] const router createRouter({ history: createWebHistory(), routes, }) // **全局前置守卫** router.beforeEach(async (to, from, next) { const userStore useUserStore() // 1. 检查目标路由是否需要认证 if (to.meta.requiresAuth false) { next() // 不需要认证直接放行如登录页 return } // 2. 检查用户是否已登录是否有 token if (!userStore.isLoggedIn) { // 未登录重定向到登录页并携带目标路径方便登录后回跳 next(/login?redirect${encodeURIComponent(to.fullPath)}) return } // 3. 如果已登录尝试获取用户信息防止刷新页面后 store 信息丢失 // 注意fetchUserInfo 应该被设计成幂等的且不会重复请求 if (Object.keys(userStore.userInfo).length 0) { try { await userStore.fetchUserInfo() } catch (error) { // 获取用户信息失败可能是 token 无效清除状态并跳转登录 userStore.clearUserInfo() next(/login?redirect${encodeURIComponent(to.fullPath)}) return } } // 4. 检查路由的权限要求 const requiredPermission to.meta.permission if (requiredPermission) { // 使用我们 store 里的 hasPermission 方法进行校验 const hasPerm userStore.hasPermission(requiredPermission) if (!hasPerm) { // 没有权限可以跳转到无权限提示页或首页 next({ name: Forbidden }) // 假设你有一个 403 页面 // 或者 ElMessage.warning(您没有访问该页面的权限) return } } // 5. 所有检查都通过允许导航 next() }) export default router路由守卫就像一道安检门每个想进入页面的请求都必须在这里接受检查。它确保了用户只能访问被授权的页面。5.2 组件内的细粒度权限控制v-permission 指令路由守卫控制了页面入口但页面内的一个“删除”按钮该不该显示给这个用户呢这就需要更细粒度的控制。我们可以创建一个自定义指令v-permission。// src/directives/permission.js import { useUserStore } from /stores/user export const permissionDirective { mounted(el, binding) { const { value } binding // 指令的值例如 v-permission删除用户 const userStore useUserStore() if (value typeof value string) { const hasPermission userStore.hasPermission(value) if (!hasPermission) { // 如果没有权限则从 DOM 中移除该元素 el.parentNode el.parentNode.removeChild(el) // 或者隐藏元素el.style.display none } } else { // 指令值无效时可以选择移除或保留。开发阶段可以抛出警告。 console.warn(v-permission 指令需要传入一个权限字符串例如 v-permission添加用户) } } } // 在 main.js 中全局注册 // import { permissionDirective } from /directives/permission // app.directive(permission, permissionDirective)然后在组件中你就可以像下面这样轻松地控制按钮的显示与隐藏了!-- 在任意组件中 -- template div h1用户管理页面/h1 el-button typeprimary v-permission添加用户添加用户/el-button el-button typewarning v-permission编辑用户编辑/el-button el-button typedanger v-permission删除用户删除/el-button !-- 也可以用在更复杂的元素上 -- el-card v-permission查看审计日志 template #header审计日志/template !-- 日志内容 -- /el-card /div /template script setup // 如果只在当前组件使用可以局部注册指令 import { permissionDirective } from /directives/permission const vPermission permissionDirective /script这种方式非常声明式让模板代码清晰易懂。权限判断的逻辑被完全封装在指令和 Store 中组件只关心“需要什么权限”而不需要知道“如何判断权限”。5.3 动态渲染侧边栏菜单对于后台管理系统侧边栏菜单通常也需要根据权限动态生成。思路和路由守卫类似在获取用户信息后根据其权限列表过滤出有权限访问的路由然后渲染为菜单。!-- src/components/Layout/Sidebar.vue -- template el-menu :default-activeactiveMenu !-- 遍历经过权限过滤后的路由列表 -- template v-forroute in permissionedRoutes :keyroute.path el-menu-item v-if!route.children :indexroute.path component :isroute.meta.icon / {{ route.meta.title }} /el-menu-item el-sub-menu v-else :indexroute.path template #title component :isroute.meta.icon / {{ route.meta.title }} /template el-menu-item v-forchild in route.children :keychild.path :indexchild.path {{ child.meta.title }} /el-menu-item /el-sub-menu /template /el-menu /template script setup import { computed } from vue import { useRouter, useRoute } from vue-router import { useUserStore } from /stores/user const router useRouter() const route useRoute() const userStore useUserStore() // 计算属性根据用户权限过滤出可访问的路由 const permissionedRoutes computed(() { const allRoutes router.getRoutes() // 获取所有已注册的路由 // 过滤出需要显示在菜单中且有 meta.title 的路由通常是顶层路由 const mainRoutes allRoutes.filter(r r.meta?.title r.meta?.showInMenu ! false) // 进行权限过滤 return mainRoutes.filter(route { const requiredPerm route.meta.permission // 如果路由没有设置权限要求或者用户拥有该权限则显示 return !requiredPerm || userStore.hasPermission(requiredPerm) }) }) const activeMenu computed(() route.path) /script6. 安全加固与最佳实践到这里核心功能已经完成了。但要让系统更健壮我们还需要考虑一些边界情况和最佳实践。1. Token 存储安全我们选择了sessionStorage进行持久化。相比localStorage它的生命周期是会话级别的关闭浏览器标签页后自动清除安全性稍高。但对于需要“记住登录”的场景你可能需要结合localStorage或后端设置更长的refresh_token过期时间来实现。切记绝对不要将敏感的 Token 或用户信息直接写在代码或可被轻易获取的地方。2. 并发请求处理我们上面的拦截器已经通过isRefreshing标志和requestsQueue队列处理了并发 Token 刷新的问题。这是生产环境必须考虑的否则瞬间多个请求过期会导致多次刷新请求。3. 错误处理与用户体验网络错误/服务器错误在 Axios 响应拦截器的最后我们统一处理了非 401 错误可以在这里接入项目的通知组件如 ElMessage给用户友好的提示。权限不足除了跳转 403 页面也可以考虑在页面内展示一个友好的“无权限”提示组件。加载状态在fetchUserInfo或路由守卫进行权限校验时如果接口较慢可以考虑显示一个全局加载动画避免页面白屏。4. 代码组织与可维护性将userStore的hasPermission方法设计得足够通用可以支持基于角色Role或基于权限Permission的模型。将路由的权限定义 (meta.permission) 与后端返回的权限标识如权限ID或字符串保持一致最好由一份统一的枚举或常量文件来管理。对于复杂的权限模型如角色继承、数据权限可能需要在后端进行更复杂的判断前端只负责展示/隐藏核心权限校验必须依赖后端。5. 测试务必为你的权限控制逻辑编写单元测试。重点测试userStore.hasPermission在各种输入下的返回值。路由守卫在不同登录状态和权限下是否正确跳转。Axios 拦截器在 Token 过期、刷新成功/失败等场景下的行为。这套 Vue3 Pinia Axios 的认证与权限控制方案是我在多个中后台项目中打磨出来的。它结构清晰各模块职责分明从登录到权限校验形成了一条完整的闭环。刚开始搭建可能会觉得步骤不少但一旦搭好后续的开发就像搭积木一样顺畅。尤其是那个自动刷新 Token 的拦截器能省去无数手动处理登录过期的麻烦。希望这份详细的实践指南能帮你少走弯路如果你在实现过程中遇到任何问题欢迎随时交流。记住好的架构不是一次成型的而是在解决实际问题的过程中不断演进而来的。