【ruoyi前后端分离版学习】002--权限框架

概述

上篇我们大概了解了框架的整体功能,也决定好了研究路线,今天就按计划开始,从权限开始入手,我们直接查看数据表一窥究竟

从数据表我们可以看出,ruoyi的权限管理使用的是rbac模型,即基于角色的权限访问控制。用户的权限一般包括,接口权限,菜单权限,按钮权限,数据权限等,这里我们把这些权限统称为资源。rbac模型需要我们至少需要用户表,角色表,资源表,用户角色表,角色资源表,
首先是用户表:

用户表大多数字段还OK,其中一个用户类型我们先记下,看有什么用。
角色表:

菜单表:

可以看出,ruoyi把框架的权限大概分成了目录,菜单和按钮三种,这是关系到前端动态路由的主角。
接下来我们开始我们的主线:任何系统都会从登陆开始,ruoyi应该也一样,我们猜想应该是:
登陆->账号验证->获取权限->前端动态渲染,
带着我们的疑问,我们直接翻开ruoyi的登录源码:


/**
 * 登录验证
 * 
 * @author ruoyi
 */
@RestController
public class SysLoginController
{
    @Autowired
    private SysLoginService loginService;

    @Autowired
    private ISysMenuService menuService;

    @Autowired
    private SysPermissionService permissionService;

    /**
     * 登录方法
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
...

从调试台的输入输出内容及登录源码可以看出,登录成功后只返回了token。


从调试窗口可以看到,用户登录后,接口只返回了token,接下来前端马上执行了获取用户信息和路由信息的请求,而这两个请求返回的数据正是动态渲染的数据,我们继续深入这两个请求:

    /**
     * 获取用户信息
     * 
     * @return 用户信息
     */
    @GetMapping("getInfo")
    public AjaxResult getInfo()
    {
        SysUser user = SecurityUtils.getLoginUser().getUser();
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // 权限集合
        Set<String> permissions = permissionService.getMenuPermission(user);
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }

源码中我们看到,这个接口返回了用户信息,角色集合,权限集合,具体内容我们看控制台的返回信息:

我们可以清楚看到,权限返回一段符号,具体什么用我们还需要深入,角色返回的是角色名称。
接下来是路由信息接口:

    /**
     * 获取路由信息
     * 
     * @return 路由信息
     */
    @GetMapping("getRouters")
    public AjaxResult getRouters()
    {
        Long userId = SecurityUtils.getUserId();
        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
        return AjaxResult.success(menuService.buildMenus(menus));
    }


其实本篇我们把前后端的权限动态渲染整明白了,就事就算结了。我们直接看前端vue代码
我们看到,返回的路由信息里没有首页,但是导航栏中确有首页。其实若依把路由分成了静态路由和动态路由量部分,
静态路由代表那些不需要动态判断权限的路由,如登录页、404、等通用页面,在@/router/index.js配置对应的公共路由

// 公共路由
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login'),
    hidden: true
  },
  {
    path: '/register',
    component: () => import('@/views/register'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error/401'),
    hidden: true
  },
  {
    path: '',
    component: Layout,
    redirect: 'index',
    children: [
      {
        path: 'index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]
  },
  {
    path: '/user',
    component: Layout,
    hidden: true,
    redirect: 'noredirect',
    children: [
      {
        path: 'profile',
        component: () => import('@/views/system/user/profile/index'),
        name: 'Profile',
        meta: { title: '个人中心', icon: 'user' }
      }
    ]
  }
]

动态路由代表那些需要根据用户动态判断权限并通过addRoutes动态添加的页面,不通的用户不通的角色拥有不用的目录菜单,因此返回不通的路由信息。
我们可以从1个请求的最开始分析,我们第一次进入了系统,还没登录过,进入如下逻辑:scr/permission.js

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'

NProgress.configure({ showSpinner: false })

const whiteList = ['/login', '/auth-redirect', '/bind', '/register']

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
   ...
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

我们先分析没登录的逻辑,如果没有本地没token说明没登录过,如果在白名单内则直接进入,如果不是则任何路由都跳转到登录页。
我们进入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 => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

登陆成功了再cookie中保存了ADMIN-TOKEN


登录完成后,路由进入"/"根路径,因此触发了全局前置守卫执行,这个时候本地已经有了token,因此走了有token的逻辑,如下:


router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    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 {
    ...
  }
})
    // 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          const user = res.user
          const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_NAME', user.userName)
          commit('SET_AVATAR', avatar)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    },


返回的数据中比较重要的是权限标识字符串,这个代表着用户可以进行哪些操作,对前端而言,是进哪些按钮进行渲染。

接下来就是本篇最关键的部分,怎么生成动态路由。

从路由守卫里看出,去请求了获取路由信息接口:

// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
  return asyncRouterMap.filter(route => {
    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
}

用户的路由有如下几种:
一类静态路由:这些路由不需要权限,如登录,注册,错误页等。
二类前端预置的动态路由:这类路由需要权限但没有菜单入口,但是可以跳转,如分配角色和分配账号,字典数据等页面
三类后台返回的路由:这类路由需要权限有菜单入口
现在我们来捋一遍思路:
首先router/index.js生成的路由只包含了一类静态路由,接着在GenerateRoutes方法中按照用户权限加入二类路由,最后在router.beforeEach方法中加入三类路由。

导航栏在SidebarItem中进行渲染

posted @   今晚可以打老虎  阅读(3282)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示