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

编辑

若依框架登录鉴权: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至vuex
            this.$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.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
           //!!!key!!!
          //获取并设置后端传过来的token
          setToken(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.user
          const 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);
    }

管理员 

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

编辑

 普通员工

若依框架登录鉴权详解(动态路由)_拦截器_03

编辑

根据权限动态生成并添加路由

步骤 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/svg breadcrumb: 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 {
    // 没有token
    if (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

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

编辑

//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); //array
          const 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.path
          if (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.js

import store from '@/store'

function authPermission(permission) {
  const all_permission = "*:*:*";
  const permissions = store.getters && store.getters.permissions
  if (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.roles
  if (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),并在其中检查路由是否已经存在于路由表中。然而,这种方法比较复杂且可能不够直观,因为它依赖于路由守卫的多次调用和状态检查。

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