前端登录鉴权——以若依Ruoyi前后端分离项目为例解读

权限模型

 Ruoyi框架学习——权限管理_若依框架权限-CSDN博客

用户-角色-菜单(User-Role-Menu)模型是一种常用于权限管理的设计模式,用于实现系统中的用户权限控制。该模型主要包含以下几个要素:

  1. 用户(User):系统中的具体操作者,可以是人员、系统账号等。每个用户都有自己的身份识别信息,例如用户名、密码等。

  2. 角色(Role):是对用户的一种分类或分组,每个角色可以包含一组权限或权限集合。通常,用户可以被分配到一个或多个角色,而不是直接分配权限给用户。角色可以是预定义的,也可以是根据业务需求动态创建的。

  3. 菜单(Menu):系统中的功能或操作项,例如菜单、页面、按钮等。菜单通常与权限相关联,只有具有相应权限的用户或角色才能访问或执行对应的菜单项。

 登录鉴权方式

前端登录鉴权方式是指在前端页面中对用户进行身份验证和权限验证的过程。以下是一些常见的前端登录鉴权方式:

1. 用户名密码登录

这是最基本的登录方式,用户在登录页面输入用户名和密码,前端将这些信息发送到后端进行验证。后端通过验证后返回一个包含用户信息的令牌(Token),前端将这个令牌保存起来(如存储在Cookie、LocalStorage或SessionStorage中)以后的请求都需要带上这个令牌进行验证

2. 扫码登录

扫码登录通常用于移动应用或桌面应用与网页之间的登录。用户在前端页面使用手机扫描二维码,后端生成一个唯一的登录凭证(Ticket),前端轮询后端接口检查该凭证是否已被使用。一旦凭证被使用,后端通过验证后返回一个包含用户信息的令牌,前端保存起来进行后续请求的验证。

3. 第三方登录

第三方登录允许用户使用第三方平台(如微信、QQ、微博等)的账号进行登录。前端将用户的登录凭证(如OpenID)发送到后端进行验证。后端通过验证后返回一个包含用户信息的令牌,前端保存起来进行后续请求的验证。这种方式简化了用户的登录流程,提高了用户体验。

4. HTTP Basic Authentication

HTTP Basic Authentication 是一种简单的鉴权方式,客户端在请求时通过用户名+密码的方式实现对用户身份的验证。然而,这种方式存在安全性问题,因为用户名和密码以Base64编码方式加密,并不安全。此外,HTTP协议没有提供机制清除浏览器中的Basic认证信息,除非关闭标签页、浏览器或清除历史记录。

5. Session-Cookie鉴权

Session-Cookie鉴权是利用服务端的Session(会话)和浏览器(客户端)的Cookie来实现前后端通信认证模式。客户端发送登录信息到服务器,服务器校验成功后生成Session ID并保存在服务端,同时通过Set-Cookie响应头将Session ID发送给客户端。客户端在后续请求中携带Session ID(通常放在Cookie中),服务器通过验证Session ID来确认用户身份。这种方式依赖于Cookie,存在被篡改和CSRF攻击的风险

6. Token鉴权

Token鉴权是一种无状态的鉴权方式,服务端不保存用户的认证信息或会话信息。用户登录成功后,服务端生成一个加密的Token令牌并返回给客户端。客户端在后续请求中携带Token令牌,服务端通过验证Token令牌的有效性来确认用户身份。Token令牌通常包含用户唯一身份标识、时间戳和签名等信息,以保证安全性和时效性。常见的Token鉴权方式有JWT(JSON Web Token)等。

7. JWT(JSON Web Token)鉴权

JWT是一种基于JSON的开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。JWT由头部(Header)、负载(Payload)和签名(Signature)三部分组成,通过这三部分可以验证信息的真实性和完整性。JWT通常用于在用户登录后生成一个包含用户信息的Token令牌,并在后续请求中携带该Token令牌进行身份验证。

8. OAuth(开放授权)

OAuth是一种开放授权标准,允许用户在不提供用户名和密码的情况下,授权第三方应用访问他们在特定服务上存储的敏感信息。OAuth通过令牌(Token)来代替密码进行身份验证和授权,从而提高了安全性。OAuth通常用于第三方登录和API访问等场景

不同用户登录显示不同菜单栏(登录鉴权) 

1.常见思路

在前端实现基于角色+自定义权限或角色+数据字典的用户角色权限判断,并据此返回不同的导航菜单,通常涉及以下几个步骤:

1. 后端准备

a. 角色与权限定义

  • 在后端数据库中定义角色(Roles)和权限(Permissions)。每个角色可以关联多个权限。
  • 权限可以是具体的操作(如“编辑文章”、“删除用户”等),也可以是访问特定页面的权限。

b. 数据字典

  • 如果使用数据字典来控制某些动态内容(如导航菜单项),则需要在后端定义这些数据字典,并关联到相应的权限或角色。

c. 用户角色与权限分配

  • 为每个用户分配一个或多个角色,这些角色决定了用户的权限。

d. API 接口

  • 提供一个或多个API接口,用于前端查询当前登录用户的角色、权限以及可能的数据字典信息。
2. 前端实现

a. 登录与获取权限

  • 用户登录后,前端通过API调用获取当前用户的角色、权限及可能的数据字典信息。
  • 将这些信息存储在前端的全局状态管理(如Redux、Vuex等)中,以便在多个组件中共享。

b. 导航菜单生成

  • 根据获取到的权限或数据字典信息,动态生成导航菜单。
  • 这通常涉及到遍历权限列表或数据字典,检查每个菜单项是否对当前用户可见。

c. 示例代码

假设你使用Vue.js和Vuex来实现:

// Vuex store  
const store = new Vuex.Store({  state: {  userPermissions: [], // 存储用户权限  navigationItems: [] // 存储导航菜单项  },  mutations: {  setUserPermissions(state, permissions) {  state.userPermissions = permissions;  // 根据权限生成导航菜单(这里仅为示例,实际逻辑可能更复杂)  state.navigationItems = permissions.map(permission => {  // 假设每个权限对象中包含一个字段用于指示对应的菜单项  return {  title: permission.menuTitle,  path: permission.menuPath,  visible: true // 根据实际逻辑判断是否需要显示  };  }).filter(item => item.visible);  }  },  actions: {  fetchUserPermissions({ commit }) {  // 调用API获取用户权限  axios.get('/api/user/permissions').then(response => {  commit('setUserPermissions', response.data);  });  }  }  
});  // Vue组件中使用  
<template>  <nav>  <ul>  <li v-for="item in navigationItems" :key="item.path">  <router-link :to="item.path">{{ item.title }}</router-link>  </li>  </ul>  </nav>  
</template>  <script>  
export default {  computed: {  navigationItems() {  return this.$store.state.navigationItems;  }  },  mounted() {  this.$store.dispatch('fetchUserPermissions');  }  
}  
</script>

注意

  • 上面的代码仅为示例,实际项目中可能需要更复杂的逻辑来处理权限和导航菜单的生成。
  • 权限和菜单项的对应关系可能不是直接存储在权限对象中的,而是需要通过额外的逻辑或数据字典来映射。
  • 安全性考虑:确保前端不会直接暴露敏感信息,所有敏感操作都应在后端进行验证。

 若依框架(Ruoyi)后端的登录权限身份认证流程是一个复杂但高效的过程,它确保了系统的安全性和数据的保护。以下是一个典型的若依框架后端登录权限身份认证流程,基于多个来源的信息进行归纳和整理:

项目详解

1.发起请求获取认证凭证(token)

现象:用户未登录或者token过期,刷新页面将重定向至登录页面

  • 如果用户身份验证通过,系统会生成一个认证凭证(如JWT,即JSON Web Token)。
    • 认证凭证的生成过程可能包括生成一个唯一的UUID作为token的一部分,并设置token的有效期(这个有效期可能是通过Redis等缓存系统来控制的,因为JWT本身不直接支持自动刷新有效期)。
    • 生成的token会包含用户的基本信息和权限信息,以便后续进行权限控制和身份验证。
    • 后端通过验证后返回一个包含用户信息的令牌(Token),前端将这个令牌保存起来(如存储在Cookie、LocalStorage或SessionStorage中)以后的请求都需要带上这个令牌进行验证
​
//login.vue
handleLogin() {this.$refs.loginForm.validate(valid => {if (valid) {this.loading = true;if (this.loginForm.rememberMe) {Cookies.set("username", this.loginForm.username, { expires: 30 });Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });} else {Cookies.remove("username");Cookies.remove("password");Cookies.remove('rememberMe');}this.$store.dispatch("Login", this.loginForm).then(() => {//存储title至vuexthis.$store.commit("settings/SET_HEADTITLE",this.title)this.$router.push({ path: this.redirect || "/" }).catch(()=>{});}).catch(() => {this.loading = false;if (this.captchaEnabled) {this.getCode();}});}});}
//store/modules/user.js
// 前端登录请求Login({ commit }, userInfo) {const username = userInfo.username.trim()const password = userInfo.passwordconst code = userInfo.codeconst uuid = userInfo.uuidreturn new Promise((resolve, reject) => {login(username, password, code, uuid).then(res => {//!!!key!!!//获取并设置后端传过来的tokensetToken(res.data.token)commit('SET_TOKEN', res.data.token)resolve()}).catch(error => {reject(error)})})},
//system/SysLoginController
//后端登录响应/*** 登录方法** @param loginBody 登录信息* @return 结果*/@SaIgnore@PostMapping("/login")public R<Map<String, Object>> login(@Validated @RequestBody LoginBody loginBody) {Map<String, Object> ajax = new HashMap<>();// 生成令牌String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());ajax.put(Constants.TOKEN, token);return R.ok(ajax);}​

若依框架前端怎样判断token是否过期,并且要重新登录获取token:响应拦截器

在前端应用中,使用响应拦截器(response interceptor)来判断token是否过期,并在需要时重新登录以获取新的token是一种常见的做法。这通常涉及到以下几个步骤:

  1. 设置响应拦截器:在发起HTTP请求的库(如axios)中设置拦截器,以便在接收到响应时执行自定义逻辑。

  2. 检查响应状态码:在拦截器中检查响应的状态码,特别是那些表明需要身份验证的状态码(如401、403等)。

  3. 判断token是否过期:虽然401状态码通常表示未授权,但具体是否因为token过期导致需要额外的逻辑来判断。有时候,后端会在响应头中返回一个特定的字段(如WWW-Authenticate)来指示这一点,但更常见的是通过解析响应体中的信息(如错误信息)来判断。不过,在某些情况下,你可能只是简单地根据收到了401就认为token已过期。

  4. 重新登录获取token:如果确定token已过期,你可以尝试使用某种形式的“刷新token”来自动获取新的access token,或者引导用户重新登录。这取决于你的应用是否支持无感知重新登录(使用refresh token)以及后端的实现。

  5. 更新本地存储和重试请求:如果成功获取了新的token,更新本地存储中的token,并根据需要重试之前的请求。

  6. 错误处理:处理在重新登录过程中可能出现的任何错误,并向用户显示适当的反馈。

//utils/request.js
// 响应拦截器
service.interceptors.response.use(res => {// 未设置状态码则默认成功状态const code = res.data.code || 200;// 获取错误信息const msg = errorCode[code] || res.data.msg || errorCode['default']// 二进制数据则直接返回if (res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer') {return res.data}if (code === 401) {if (!isRelogin.show) {isRelogin.show = true;MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {isRelogin.show = false;store.dispatch('LogOut').then(() => {location.href = process.env.VUE_APP_CONTEXT_PATH + "index";})}).catch(() => {isRelogin.show = false;});}return Promise.reject('无效的会话,或者会话已过期,请重新登录。')}// else if (code === 500) {//   Message({ message: msg, type: 'error' })//redis错误,待解决//   return Promise.reject(new Error(msg))// }else if (code === 601) {Message({ message: msg, type: 'warning' })return Promise.reject('error')} else if (code !== 200) {Notification.error({ title: msg })return Promise.reject('error')} else {return res.data}},error => {console.log('err' + error)let { message } = error;if (message == "Network Error") {message = "后端接口连接异常";} else if (message.includes("timeout")) {message = "系统接口请求超时";} else if (message.includes("Request failed with status code")) {message = "系统接口" + message.substr(message.length - 3) + "异常";}Message({ message: message, type: 'error', duration: 5 * 1000 })return Promise.reject(error)}
)

若报401错误表明token到期,401错误是一种常见的HTTP状态码,它表示“未授权”(Unauthorized)访问。当用户尝试访问受保护的资源时,如果未能提供有效的身份验证信息或提供的身份验证信息被拒绝,服务器就会返回401错误。这种错误通常出现在需要登录或授权才能访问的网站上,如银行网站、社交媒体网站等。

2. 获取认证信息(user、roles and permission)

不同用户的权限不同,右侧导航条【大层面】和页面中操作按钮【小层面】不同

  • 将生成的token和用户的相关信息(如用户ID、角色、权限等)缓存起来,以便后续快速验证和查询。
//store/modules/user.js// 获取用户信息【前端请求】GetInfo({ commit, state }) {return new Promise((resolve, reject) => {getInfo().then(res => {const user = res.data.userconst avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : user.avatar;if (res.data.roles && res.data.roles.length > 0) { // 验证返回的roles是否是一个非空数组commit('SET_ROLES', res.data.roles)commit('SET_PERMISSIONS', res.data.permissions)} else {commit('SET_ROLES', ['ROLE_DEFAULT'])}commit('SET_NAME', user.userName)commit('SET_AVATAR', avatar)resolve(res)}).catch(error => {reject(error)})})},//system/SysLoginController
//后端响应返回用户信息【user、roles and permisssions】/*** 获取用户信息** @return 用户信息*/@GetMapping("getInfo")public R<Map<String, Object>> getInfo() {LoginUser loginUser = LoginHelper.getLoginUser();SysUser user = userService.selectUserById(loginUser.getUserId());Map<String, Object> ajax = new HashMap<>();ajax.put("user", user);ajax.put("roles", loginUser.getRolePermission());ajax.put("permissions", loginUser.getMenuPermission());return R.ok(ajax);}

管理员 

 普通员工

根据权限动态生成并添加路由
步骤 1: 定义静态路由和异步路由

首先,你需要定义你的路由,通常分为静态路由(不需要权限即可访问的路由,如登录页、404页面等)和异步路由(需要权限的路由,如用户管理、订单管理等)。

/*** Note: 路由配置项** hidden: true                     // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1* alwaysShow: true                 // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面*                                  // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面*                                  // 若你想不管路由下面的 children 声明的个数都显示你的根路由*                                  // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由* redirect: noRedirect             // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击* name:'router-name'               // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数* roles: ['admin', 'common']       // 访问路由的角色权限* permissions: ['a:a:a', 'b:b:b']  // 访问路由的菜单权限* meta : {noCache: true                   // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)title: 'title'                  // 设置该路由在侧边栏和面包屑中展示的名字icon: 'svg-name'                // 设置该路由的图标,对应路径src/assets/icons/svgbreadcrumb: false               // 如果设置为false,则不会在breadcrumb面包屑中显示activeMenu: '/system/user'      // 当路由设置了该属性,则会高亮相对应的侧边栏。}*/

// router/index.js  import Vue from 'vue'  
import Router from 'vue-router'  Vue.use(Router)  // 静态路由  
const constantRoutes = [  {  path: '/login',  component: () => import('@/views/login/index'),  hidden: true  },  {  path: '/404',  component: () => import('@/views/404'),  hidden: true  }  
]  // 异步路由(示例)  
const asyncRoutes = [  {  path: '/user',  component: () => import('@/views/user/index'),  meta: { title: '用户管理', roles: ['admin', 'editor'] }  },  // 更多路由...  
]  const createRouter = () => new Router({  // mode: 'history', // 需要后端支持  scrollBehavior: () => ({ y: 0 }),  routes: constantRoutes  
})  const router = createRouter()  // 动态添加路由  
function addAsyncRoutes(routes) {  routes.forEach(route => {  router.addRoute(route)  })  
}  export {  router,  addAsyncRoutes,  asyncRoutes  
}
步骤 2:  路由守卫与权限验证和路由添加

在用户登录后,你需要从后端获取用户的权限信息,并根据这些权限信息来过滤和添加路由。

你还需要设置路由守卫来确保用户在没有权限时不能访问某些页面。

//src/permission.js
//前端路由守卫
router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {//获取路由的mata.title属性,并存储在Vuex中to.meta.title && store.dispatch('settings/setTitle', to.meta.title)/* has token*/if (to.path === '/login') {next({ path: '/' })NProgress.done()} else {if (store.getters.roles.length === 0) {isRelogin.show = true// 判断当前用户是否已拉取完user_info信息store.dispatch('GetInfo').then(() => {isRelogin.show = false//生成路由store.dispatch('GenerateRoutes').then(accessRoutes => {// 根据roles权限生成可访问的路由表//添加路由router.addRoutes(accessRoutes) // 动态添加可访问路由表next({ ...to, replace: true }) // hack方法 确保addRoutes已完成})}).catch(err => {store.dispatch('LogOut').then(() => {Message.error(err)next({ path: '/' })})})} else {next()}}} else {// 没有tokenif (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入next()} else {next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页NProgress.done()}}
})

获取与添加**路由** 

 

  //生成路由store.dispatch('GenerateRoutes').then(accessRoutes => {// 根据roles权限生成可访问的路由表//添加路由router.addRoutes(accessRoutes) // 动态添加可访问路由表next({ ...to, replace: true }) // hack方法 确保addRoutes已完成})
 GenerateRoutes

请求得到的sdata与rdata

//store/module/permisssion.js// 生成路由GenerateRoutes({ commit }) {return new Promise(resolve => {// 向后端请求路由数据getRouters().then(res => {const sdata = JSON.parse(JSON.stringify(res.data))const rdata = JSON.parse(JSON.stringify(res.data))//console.log(sdata,rdata); //arrayconst sidebarRoutes = filterAsyncRouter(sdata)const rewriteRoutes = filterAsyncRouter(rdata, false, true)// 动态路由,基于用户权限动态去加载const asyncRoutes = filterDynamicRoutes(dynamicRoutes);rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })router.addRoutes(asyncRoutes);commit('SET_ROUTES', rewriteRoutes)commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))commit('SET_DEFAULT_ROUTES', sidebarRoutes)commit('SET_TOPBAR_ROUTES', sidebarRoutes)resolve(rewriteRoutes)})})}// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {return asyncRouterMap.filter(route => {//type标识sidebarRoutes(false)与rewriteRoutes(true)if (type && route.children) {route.children = filterChildren(route.children)}if (route.component) {// Layout ParentView 组件特殊处理if (route.component === 'Layout') {route.component = Layout} else if (route.component === 'ParentView') {route.component = ParentView} else if (route.component === 'InnerLink') {route.component = InnerLink} else {//指定目录下找对应组件route.component = loadView(route.component)}}if (route.children != null && route.children && route.children.length) {//递归route.children = filterAsyncRouter(route.children, route, type)} else {delete route['children']delete route['redirect']}return true})
}function filterChildren(childrenMap, lastRouter = false) {var children = []childrenMap.forEach((el, index) => {if (el.children && el.children.length) {if (el.component === 'ParentView' && !lastRouter) {el.children.forEach(c => {c.path = el.path + '/' + c.pathif (c.children && c.children.length) {children = children.concat(filterChildren(c.children, c))return}children.push(c)})return}}if (lastRouter) {el.path = lastRouter.path + '/' + el.path}children = children.concat(el)})return children
}export const loadView = (view) => {if (process.env.NODE_ENV === 'development') {return (resolve) => require([`@/views/${view}`], resolve)} else {// 使用 import 实现生产环境的路由懒加载return () => import(`@/views/${view}`)}
}
//权限判断
//store/modules/permission.js// 动态路由遍历,验证是否具备权限export function filterDynamicRoutes(routes) {const res = []routes.forEach(route => {if (route.permissions) {if (auth.hasPermiOr(route.permissions)) {res.push(route)}} else if (route.roles) {if (auth.hasRoleOr(route.roles)) {res.push(route)}}})return res
}//plugins/auth.jsimport store from '@/store'function authPermission(permission) {const all_permission = "*:*:*";const permissions = store.getters && store.getters.permissionsif (permission && permission.length > 0) {return permissions.some(v => {return all_permission === v || v === permission})} else {return false}
}function authRole(role) {const super_admin = "admin";const roles = store.getters && store.getters.rolesif (role && role.length > 0) {return roles.some(v => {return super_admin === v || v === role})} else {return false}
}export default {// 验证用户是否具备某权限hasPermi(permission) {return authPermission(permission);},// 验证用户是否含有指定权限,只需包含其中一个hasPermiOr(permissions) {return permissions.some(item => {return authPermission(item)})},// 验证用户是否含有指定权限,必须全部拥有hasPermiAnd(permissions) {return permissions.every(item => {return authPermission(item)})},// 验证用户是否具备某角色hasRole(role) {return authRole(role);},// 验证用户是否含有指定角色,只需包含其中一个hasRoleOr(roles) {return roles.some(item => {return authRole(item)})},// 验证用户是否含有指定角色,必须全部拥有hasRoleAnd(roles) {return roles.every(item => {return authRole(item)})}
}
 addRoute
 router.addRoutes(accessRoutes) // 动态添加可访问路由表next({ ...to, replace: true }) // hack方法 确保addRoutes已完成

第一行:router.addRoutes(accessRoutes)

这行代码的作用是动态地向Vue Router的路由表中添加路由。addRoutes方法是Vue Router早期版本(Vue Router 2.x)中用于动态添加路由的方法。然而,需要注意的是,在Vue Router 3.x及更高版本中,官方推荐使用addRoutes的替代方案,即通过router.matcher.addRoutes或者完全重新创建router实例来动态添加路由(因为addRoutes方法在Vue Router 4.x中被移除)。但这里我们仍然以addRoutes为例进行说明。

  • router:这是Vue Router的实例,它包含了应用的所有路由配置。
  • addRoutes:这是Vue Router实例上的一个方法,用于向路由表中添加新的路由规则。
  • accessRoutes:这是一个数组,包含了要添加到路由表中的路由对象。每个路由对象都遵循Vue Router的路由配置规范,包括pathcomponentchildren等属性。

第二行:next({ ...to, replace: true })

这行代码通常出现在Vue Router的导航守卫(Navigation Guards)中,用于控制路由的跳转。next函数是导航守卫的回调函数,它决定了路由跳转的行为。

  • next:这是一个必须调用的函数,用于解决守卫中的钩子。调用它时,可以传入一个位置对象或者一个错误。如果不调用,则整个路由导航都将被“挂起”。
  • { ...to, replace: true }:这里使用了对象展开语法(...)来复制to对象的所有属性到一个新对象中,并添加或修改replace属性为trueto对象通常包含了即将要跳转到的路由信息,如pathquery等。
    • replace: true:这个选项的作用是,当使用next函数进行路由跳转时,不是将新路由添加到历史记录堆栈中,而是替换掉当前的路由。这通常用于避免在路由跳转后留下无法退回到当前路由的“死胡同”。

结合使用这两行代码的场景

在实际应用中,这两行代码经常一起使用在基于用户权限的路由控制中。比如,当用户登录后,后端返回了用户的权限信息,前端根据这些权限信息动态生成可访问的路由表(accessRoutes),并通过router.addRoutes(accessRoutes)将这些路由添加到路由表中。然后,在导航守卫中,使用next({ ...to, replace: true })来确保用户被重定向到目标路由,并且由于replace: true,这个跳转不会在历史记录中留下当前路由的入口,从而避免了用户通过浏览器的前进/后退按钮访问到未授权的路由。

使用路由守卫保证addRoute执行完成:你可以设置一个全局前置守卫(beforeEach)或后置守卫(afterEach),并在其中检查路由是否已经存在于路由表中。然而,这种方法比较复杂且可能不够直观,因为它依赖于路由守卫的多次调用和状态检查。

根据权限限制页面的操作按钮(新增、修改和删除操作等)

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/418922.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全倒装COB超微小间距LED显示屏的工艺技术,相比SMD小间距有何优势

全倒装COB&#xff08;Chip On Board&#xff09;超微小间距LED显示屏&#xff0c;在工艺技术上的革新&#xff0c;相较于传统的SMD&#xff08;Surface Mount Device&#xff09;小间距LED显示屏&#xff0c;展现出了多方面的显著优势。 首先&#xff0c;全倒装技术极大地提升…

【踩坑】Vue3项目正常跑动后页面空白问题

近期踩了个坑&#xff0c;Vue3搭建的项目能够正常跑动&#xff0c;但是页面却是空白的&#xff0c;控制台也不报错&#xff0c;只留下一行警告&#xff1a; 发现是 router 入口文件&#xff08;一般是在 router 文件夹下的 index 里面&#xff09;的写法和 vite 版本不匹配的问…

代码随想录 刷题记录-24 图论 (1)理论基础 、深搜与广搜

一、理论基础 参考&#xff1a; 图论理论基础 深度优先搜索理论基础 广度优先搜索理论基础 dfs dfs搜索可一个方向&#xff0c;并需要回溯&#xff0c;所以用递归的方式来实现是最方便的。 有递归的地方就有回溯&#xff0c;例如如下代码&#xff1a; void dfs(参数) {…

基于 RocketMQ 的云原生 MQTT 消息引擎设计

作者&#xff1a;沁君 概述 随着智能家居、工业互联网和车联网的迅猛发展&#xff0c;面向 IoT&#xff08;物联网&#xff09;设备类的消息通讯需求正在经历前所未有的增长。在这样的背景下&#xff0c;高效和可靠的消息传输标准成为了枢纽。MQTT 协议作为新一代物联网场景中…

QT+OSG+osg-earth显示一个球

目录 1、环境配置 2、在QT Creator导入相关的库 3、代码部分 4、运行过程中的问题 5、相关参考 重要衔接&#xff1a;QTOSG显示一个三维模型-CSDN博客 1、环境配置 系统&#xff1a;windows10系统 QT:版本5.15.2 编译器&#xff1a;MSVC2019_64bit 编辑器…

unity 实现吸血鬼幸存者的随机奖励

设置奖励的数据类型 // // Auto Generated Code By excel2json // https://neil3d.gitee.io/coding/excel2json.html // 1. 每个 Sheet 形成一个 Struct 定义, Sheet 的名称作为 Struct 的名称 // 2. 表格约定&#xff1a;第一行是变量名称&#xff0c;第二行是变量类型// Gen…

DevC++编译及使用Opencv

1.依赖 需要如下依赖&#xff1a; DevC11Opencv4.10.0CMake.exe 整个安装过程参考下面的文章&#xff1a;https://blog.csdn.net/weixin_41673576/article/details/108519841 这里总结一下遇到的问题。 2.问题 2.1 DevC安装路径 一定不要有空格&#xff01;&#xff01;否则…

【自动驾驶】控制算法(八)横向控制Ⅲ | 代码与模型

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

Java | Leetcode Java题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; class Solution {static final int MASK1 1 << 7;static final int MASK2 (1 << 7) (1 << 6);public boolean validUtf8(int[] data) {int m data.length;int index 0;while (index < m) {int num data[index];…

从零开始学习JVM(七)- StringTable字符串常量池

1 概述 String应该是Java使用最多的类吧&#xff0c;很少有Java程序没有使用到String的。在Java中创建对象是一件挺耗费性能的事&#xff0c;而且我们又经常使用相同的String对象&#xff0c;那么创建这些相同的对象不是白白浪费性能吗。所以就有了StringTable这一特殊的存在&…

Python爬虫:通过js逆向获取某瓜视频的下载链接

爬虫:通过js逆向获取某瓜视频的下载链接 1. 前言2. 获取script标签下的视频加密数据3. 第一步:获取解密后的视频下载链接4. 第二步:模拟生成加密的webid值 1. 前言 就小编了解&#xff0c;某瓜视频这个网站对应视频下载链接加密处理至少经过三个版本。之前在CSDN发布了一篇关于…

华为 HCIP-Datacom H12-821 题库 (5)

有需要题库的可以看主页置顶 需要题库的加Q裙 V群仅进行学习交流 1.以下关于堆叠 MAD 检测说法错误的是&#xff1f; A、堆系统互为代理进行 MAD 检测时&#xff0c;两个堆系统可以使用相同的D omain ID B、MAD 检测的方式分为直连检测、代理检测 C、MAD 代理检测要求所有堆叠…

Vim笔记

【指尖飞舞&#xff1a;vscode vim 高效开发&#xff08;系列视频&#xff09;】https://www.bilibili.com/video/BV1z541177Jyp16&vd_source23e4761174881d73295e362ffd706749 Vscode vim插件配置-CSDN博客 g_跳到行尾最后一个非空字符 gd :go to definition ctrl (o): …

【基础】Three.js 自定义几何体和复制几何体

通过自定义顶点数据&#xff0c;可以创建任意的几何体。像threejs的长方体BoxGeometry、球体SphereGeometry等几何体都是基于BufferGeometry类构建的&#xff0c;它表示一个没有任何形状的空几何体。 1. 自定义点模型 通过javascript 类型化数组 Float32Array创建一组xyz坐标…

比特币客户端和API

1. 比特比客户端的安装 Bitcoin Core 客户端适用于从 x86 Windows 到 ARM Linux 的不同架构和平台&#xff0c;如下图所示&#xff1a; 2. Bitcoin Core客户端的类型 2.1 Bitcoind Bitcoind 末尾的字母 d 表示 daemon (守护程序&#xff09;。所谓守护程序&#xff0c;就是指常…

基于深度学习 卷积神经网络resnext50的中医舌苔分类系统

项目概述 本项目旨在通过深度学习技术&#xff0c;特别是利用卷积神经网络&#xff08;Convolutional Neural Networks, CNNs&#xff09;中的ResNeXt50架构&#xff0c;实现对中医舌象图像的自动分类。该系统不仅能够识别不同的舌苔类型&#xff0c;还能够在PyQt5框架下提供一…

简单实用的php全新实物商城系统

免费开源电商系统,提供灵活的扩展特性、高度自动化与智能化、创新的管理模式和强大的自定义模块,让电商用户零成本拥有安全、高效、专业的移动商城。 代码是全新实物商城系统源码版。 代码下载

ngrok | 内网穿透,支持 HTTPS、国内访问、静态域名

前言 当我们需要把本地开发的应用展示给外部用户时&#xff0c;常常会因为无法直接访问而陷入困境。 就为了展示一下&#xff0c;买服务、域名&#xff0c;搭环境&#xff0c;费钱又费事。 那有没有办法&#xff0c;让客户直接访问自己本机开发的应用呢&#xff1f; 这种需…

Vue 使用接口返回的背景图片和拼图图片进行滑动拼图验证

一、背景 前两天发了一篇 vue-monoplasty-slide-verify 滑动验证码插件使用及踩坑_vue-monoplasty-slide-verify 引用后不显示-CSDN博客 这两天项目又需要通过接口校验&#xff0c;接口返回了背景图片和拼图图片&#xff0c;于是在网上找了一篇帖子&#xff0c;vue 图片滑动…

安卓玩机工具------小米工具箱扩展工具 小米机型功能拓展

小米工具箱扩展版 小米工具箱扩展版 iO_Box_Mi_Ext是由晨钟酱开发的一款适用于小米&#xff08;MIUI&#xff09;、多亲&#xff08;2、2Pro&#xff09;、多看&#xff08;多看电纸书&#xff09;的多功能工具箱。该工具所有功能均可以免root实现&#xff0c;使用前&…