当前位置: 首页 > news >正文

若依框架登录鉴权详解(动态路由)

若依框架登录鉴权:1.获取token(过期在响应拦截器中实现),2.基于RBAC模型获取用户、角色和权限信息(在路由前置守卫),3.根据用户权限动态生成(从字符串->组件,根据permission添加动态路由信息)和添加路由addRoutes(在路由前置守卫)

若依框架(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),并在其中检查路由是否已经存在于路由表中。然而,这种方法比较复杂且可能不够直观,因为它依赖于路由守卫的多次调用和状态检查。

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 鸿蒙轻内核M核源码分析系列四 中断Hwi
  • Spring Boot 整合 Sentinel 实现流量控制
  • LabVIEW与Python联合图像处理
  • 2024 年高教社杯全国大学生数学建模竞赛题目【A/B/C/D/E题】完整思路
  • TensorFlow-keras介绍(一)
  • 速盾:便宜的高防 CDN 推荐,高防 CDN 能抵御 DDoS 吗?
  • 探索 JavaScript 中的 instanceof 关键字
  • 3. GIS后端工程师岗位职责、技术要求和常见面试题
  • C语言新手小白详细教程(8)ASCll编码和字符串
  • macos下的 sed命令安装与使用 gnu-sed
  • 华为 HCIP-Datacom H12-821 题库 (10)
  • 损失函数与反向传播
  • 网络编程(TCP+网络模型)
  • Intel 在人工智能领域
  • ClickHouse 的安装与基本配置
  • ➹使用webpack配置多页面应用(MPA)
  • Debian下无root权限使用Python访问Oracle
  • If…else
  • Invalidate和postInvalidate的区别
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • mysql innodb 索引使用指南
  • MySQL QA
  • node学习系列之简单文件上传
  • PAT A1017 优先队列
  • React Native移动开发实战-3-实现页面间的数据传递
  • SpiderData 2019年2月16日 DApp数据排行榜
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 关于字符编码你应该知道的事情
  • 记一次和乔布斯合作最难忘的经历
  • 检测对象或数组
  • 紧急通知:《观止-微软》请在经管柜购买!
  • 每天一个设计模式之命令模式
  • 入门到放弃node系列之Hello Word篇
  • 试着探索高并发下的系统架构面貌
  • 写代码的正确姿势
  • 学习笔记TF060:图像语音结合,看图说话
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • ​LeetCode解法汇总1410. HTML 实体解析器
  • #QT(TCP网络编程-服务端)
  • #WEB前端(HTML属性)
  • (C)一些题4
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (阿里巴巴 dubbo,有数据库,可执行 )dubbo zookeeper spring demo
  • (二十六)Java 数据结构
  • (附源码)计算机毕业设计SSM疫情居家隔离服务系统
  • (力扣)循环队列的实现与详解(C语言)
  • (四)搭建容器云管理平台笔记—安装ETCD(不使用证书)
  • (五)c52学习之旅-静态数码管
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)JAVA中的堆栈
  • .apk 成为历史!
  • .Net OpenCVSharp生成灰度图和二值图
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .NET使用HttpClient以multipart/form-data形式post上传文件及其相关参数
  • .NET中使用Redis (二)