前端登录鉴权——以若依Ruoyi前后端分离项目为例解读
权限模型
Ruoyi框架学习——权限管理_若依框架权限-CSDN博客
用户-角色-菜单(User-Role-Menu)模型是一种常用于权限管理的设计模式,用于实现系统中的用户权限控制。该模型主要包含以下几个要素:
用户(User):系统中的具体操作者,可以是人员、系统账号等。每个用户都有自己的身份识别信息,例如用户名、密码等。
角色(Role):是对用户的一种分类或分组,每个角色可以包含一组权限或权限集合。通常,用户可以被分配到一个或多个角色,而不是直接分配权限给用户。角色可以是预定义的,也可以是根据业务需求动态创建的。
菜单(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是一种常见的做法。这通常涉及到以下几个步骤:
-
设置响应拦截器:在发起HTTP请求的库(如axios)中设置拦截器,以便在接收到响应时执行自定义逻辑。
-
检查响应状态码:在拦截器中检查响应的状态码,特别是那些表明需要身份验证的状态码(如401、403等)。
-
判断token是否过期:虽然401状态码通常表示未授权,但具体是否因为token过期导致需要额外的逻辑来判断。有时候,后端会在响应头中返回一个特定的字段(如
WWW-Authenticate
)来指示这一点,但更常见的是通过解析响应体中的信息(如错误信息)来判断。不过,在某些情况下,你可能只是简单地根据收到了401就认为token已过期。 -
重新登录获取token:如果确定token已过期,你可以尝试使用某种形式的“刷新token”来自动获取新的access token,或者引导用户重新登录。这取决于你的应用是否支持无感知重新登录(使用refresh token)以及后端的实现。
-
更新本地存储和重试请求:如果成功获取了新的token,更新本地存储中的token,并根据需要重试之前的请求。
-
错误处理:处理在重新登录过程中可能出现的任何错误,并向用户显示适当的反馈。
//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的路由配置规范,包括path
、component
、children
等属性。
第二行:next({ ...to, replace: true })
这行代码通常出现在Vue Router的导航守卫(Navigation Guards)中,用于控制路由的跳转。next
函数是导航守卫的回调函数,它决定了路由跳转的行为。
next
:这是一个必须调用的函数,用于解决守卫中的钩子。调用它时,可以传入一个位置对象或者一个错误。如果不调用,则整个路由导航都将被“挂起”。{ ...to, replace: true }
:这里使用了对象展开语法(...
)来复制to
对象的所有属性到一个新对象中,并添加或修改replace
属性为true
。to
对象通常包含了即将要跳转到的路由信息,如path
、query
等。replace: true
:这个选项的作用是,当使用next
函数进行路由跳转时,不是将新路由添加到历史记录堆栈中,而是替换掉当前的路由。这通常用于避免在路由跳转后留下无法退回到当前路由的“死胡同”。
结合使用这两行代码的场景
在实际应用中,这两行代码经常一起使用在基于用户权限的路由控制中。比如,当用户登录后,后端返回了用户的权限信息,前端根据这些权限信息动态生成可访问的路由表(accessRoutes
),并通过router.addRoutes(accessRoutes)
将这些路由添加到路由表中。然后,在导航守卫中,使用next({ ...to, replace: true })
来确保用户被重定向到目标路由,并且由于replace: true
,这个跳转不会在历史记录中留下当前路由的入口,从而避免了用户通过浏览器的前进/后退按钮访问到未授权的路由。
使用路由守卫保证addRoute执行完成:你可以设置一个全局前置守卫(beforeEach)或后置守卫(afterEach),并在其中检查路由是否已经存在于路由表中。然而,这种方法比较复杂且可能不够直观,因为它依赖于路由守卫的多次调用和状态检查。
根据权限限制页面的操作按钮(新增、修改和删除操作等)
相关文章:

前端登录鉴权——以若依Ruoyi前后端分离项目为例解读
权限模型 Ruoyi框架学习——权限管理_若依框架权限-CSDN博客 用户-角色-菜单(User-Role-Menu)模型是一种常用于权限管理的设计模式,用于实现系统中的用户权限控制。该模型主要包含以下几个要素: 用户(User)…...

【Tools】大模型中的自注意力机制
摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样 🎵 方芳《摇太阳》 自注意力机制(Self-Attention)是一…...

PhotoZoom Classic 9软件新功能特性及安装激活图文教程
PhotoZoom Classic 9这款软件能够对数码图片进行放大,而且放大后的图片没有任何的品质的损坏,没有锯齿,不会失真,如果您有兴趣的话可以试试哦! PhotoZoom Classic 9软件新功能特性 通过屡获殊荣的 S-Spline XL 插值…...

【数据结构】直接插入排序
目录 一、基本思想 二、动图演示 三、思路分析 四、代码实现 五、易错提醒 六、时间复杂度分析 一、基本思想 直接插入排序(Straight Insertion Sort)是一种简单直观的排序算法,其基本思想是: 把待排序的一个记录按其关键码…...

JavaScript 实现虚拟滚动技术
虚拟滚动 虚拟滚动(有时称为 虚拟列表、虚拟滚动条)是 JavaScript 中的一种技术,旨在优化大数据量的列表渲染,尤其是当有成千上万的数据项时,直接渲染整个列表会导致性能问题。虚拟列表通过只渲染用户视口中可见的那一…...

【重学 MySQL】十八、逻辑运算符的使用
【重学 MySQL】十八、逻辑运算符的使用 AND运算符OR运算符NOT运算符异或运算符使用 XOR 关键字使用 BIT_XOR() 函数注意事项 注意事项 在MySQL中,逻辑运算符是构建复杂查询语句的重要工具,它们用于处理布尔类型的数据,进行逻辑判断和组合条件…...

关于 QImage原始数据格式与cv::Mat原始数据进行手码数据转换 的解决方法
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/141996117 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…...

前端WebSocket客户端实现
// 创建WebSocket连接 var socket new WebSocket(ws://your-spring-boot-server-url/websocket-endpoint);// 连接打开时触发 socket.addEventListener(open, function (event) {socket.send(JSON.stringify({type: JOIN, room: general})); });// 监听从服务器来的消息 socke…...

读取realsense d455双目及imu
问题定义 实时读取realsense数据喂给slam系统 代码 /** rs_d455设备 */#include <librealsense2/rs.hpp> #include <iostream>#include "rs_common_device.h"// opencv #include <opencv2/opencv.hpp>class RsD455Device: public rsCmmonDevice…...

浮点的运算
浮点数表示: N 尾数 * 基数指数 1.25 X 106 尾数一般用补码,指数一般用移码 在IEEE745中尾数可以是原码。 尾数可以表示数值的有效精度,位数越多精度越高 阶码的位数决定数的表示范围,位数越多,范围越大 对阶时&…...

对随机游走问题的分析特定行为模式的建模
从一段随机游走的数据中寻找特定的行为模式,这种问题涉及 序列模式识别 或 序列分析。处理这种问题的算法选择取决于你要找的模式的具体性质和复杂性。以下是几种可能的算法: 隐马尔可夫模型(HMM) 隐马尔可夫模型特别适合处理随…...

JVM面试(七)G1垃圾收集器剖析
概述 上一章我们说了,G1收集器,它属于里程碑式的发展,开创了面向局部收集垃圾的概念。专门针对多核处理器以及大内存的机器。在JDK9中,更是呗指定为官方的GC收集器。满足高吞吐的通知满足GC的STW停顿时间尽可能的短。 虽然现在我…...

php转职golang第一期
入局golang 基础语法:学习 Go 语言的基本语法、数据类型、流程控制等。 数据结构与算法:掌握常用的数据结构和算法。 Web 开发基础:了解 HTTP 协议、Web 开发的基本概念。 Gin 框架或其他 Web 框架:深入学习使用一种 Go 的 Web…...

java后端服务监控与告警:Prometheus与Grafana集成
Java后端服务监控与告警:Prometheus与Grafana集成 大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿! 在现代的微服务架构中,监控和告警是确保服务稳定性的关键组成部分。Pr…...

【系统架构设计师】工厂方法设计模式
工厂方法(Factory Method)模式是一种创建型设计模式,它定义了一个用于创建对象的接口,但让子类决定要实例化的类是哪一个。工厂方法让类的实例化延迟到子类中进行。 工厂方法模式的主要角色 产品(Product):定义工厂的创建对象的接口。具体产品(Concrete Product):实…...

怎样解决OpenEuler下载sdl2失败
OpenEuler 下载 sdl2失败 解决办法(使用wget中git上下载) wget https://github.com/libsdl-org/SDL/releases/download/release-2.30.6/SDL2-2.30.6.tar.gz使用yum下载,下载的最后说找不到这样的库(no match)使用 apt-get,说找不到apt-get使用curl冲gi…...

基于Python的自然语言处理系列(2):Word2Vec(负采样)
在本系列的第二篇文章中,我们将继续探讨Word2Vec模型,这次重点介绍负采样(Negative Sampling)技术。负采样是一种优化Skip-gram模型训练效率的技术,它能在大规模语料库中显著减少计算复杂度。接下来,我们将…...

每日一题|牛客竞赛|四舍五入|字符串+贪心+模拟
每日一题|四舍五入 四舍五入 心有猛虎,细嗅蔷薇。你好朋友,这里是锅巴的C\C学习笔记,常言道,不积跬步无以至千里,希望有朝一日我们积累的滴水可以击穿顽石。 四舍五入 题目: 牛牛发明了一种新的四舍五…...

大数据之Flink(六)
17、Flink CEP 17.1、概念 17.1.1、CEP CEP是“复杂事件处理(Complex Event Processing)”的缩写;而 Flink CEP,就是 Flink 实现的一个用于复杂事件处理的库(library)。 总结起来,复杂事件处…...

设计模式学习[5]---装饰模式
文章目录 前言1. 原理阐述2. 举例2.1 人装饰方案一2.2 人装饰方案二2.3 人装饰方案三 总结 前言 近期在给一个已有的功能拓展新功能时,基于原有的设计类图进行讨论。其中涉及到了装饰模式,因为书本很早已经看过一遍,所以谈及到这个名词的时候…...

3.C_数据结构_栈
概述 什么是栈: 栈又称堆栈,是限定在一段进行插入和删除操作的线性表。具有后进先出(LIFO)的特点。 相关名词: 栈顶:允许操作的一端栈底:不允许操作的一端空栈:没有元素的栈 栈的作用: 可…...

Debian11安装DolphinScheduler
安装地址 前置准备工作 JDK安装 下载JDK (1.8),安装并配置 JAVA_HOME 环境变量,并将其下的 bin 目录追加到 PATH 环境变量中。如果你的环境中已存在,可以跳过这步 二进制包安装DolphinScheduler 依赖 apt-get install psmisc 二进制安…...

C语言深度剖析--不定期更新的第五弹
const关键字 来看一段代码: #include <stdio.h> int main() {int a 10;a 20;printf("%d\n", a);return 0; }运行结果如下: 接下来我们在上面的代码做小小的修改: #include <stdio.h> int main() {const int a 1…...

python之事务
事务(Transaction)是数据库管理系统(DBMS)中的一个重要概念,用于确保一组数据库操作要么全部成功,要么全部失败,从而保证数据的一致性和完整性。 事务ACID 特性 事务具有以下四个特性…...

文件加密软件都有哪些?推荐6款文件加密工具
不久前,一家知名科技公司的内部文件在未经授权的情况下被泄露到了网络上,其中包括了公司的核心技术蓝图、客户名单及未来战略规划。这一事件不仅给公司带来了巨大的经济损失,还严重损害了企业的声誉。 如何防止以上事件的发生呢,文…...

Docker中的容器内部无法使用vi命令怎么办?
不知道你是否遇到过,在修改容器内部的配置的时候,有时候会提示vi命令不可用。尝试去安装vi插件,好像也不是很容易,有什么办法可以帮助我们修改这个配置文件呢? 解决办法 这时候,我们就需要用到docker cp 命令了,它可以帮助我们把容器内部的文件复制到宿主机上,也可以将…...

【Linux系统编程】TCP实现--socket
使用套接字socket实现服务器和客户端之间的TCP通信。 流程如下: 实现代码: /* server.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <s…...

企业微信hook协议接口,聚合群聊客户管理工具开发
服务提供了丰富的API和SDK,可以在企微的功能之上进行应用开发和功能扩展 自建应用可以调用企微hook或协议提供的接口来实现数据交互,可以直接调用hook或协议接口提供的功能来进行消息的发送与接收、用户管理、应用管理等操作,通过接口可以实…...

Selenium集成Sikuli基于图像识别的自动化测试
看起来您提供了一个链接,但目前我并没有从该链接获取到具体的信息内容。不过,如果您希望了解如何将Sikuli集成到Selenium中,我可以为您提供一些基本的指南。 什么是Sikuli? Sikuli是一款开源工具,用于基于图像识别的自动化测试。它可以识别屏幕上的图像,并模拟用户的交…...

【STM32实物】基于STM32设计的智能仓储管理系统(程序代码电路原理图实物图讲解视频设计文档等)——文末资料下载
基于STM32设计的智能仓储管理系统 演示视频: 基于STM32设计的智能仓储管理系统 摘要 近年来,随着我国仓储发展的和药品需求的不断增多,许多医院都采用药物仓储管理系统。我国的药物仓储产业已经有了长足的发展,仓库的规模不断变大,对仓储的要求也不断增高,药物的存储,…...