天天看點

RuoYi-Vue————權限管理

RuoYi-Vue————權限管理

    • 1. 若依架構權限分類
    • 2. 若依架構權限的依次介紹
    • 3. 若依架構重要接口執行流程

1. 若依架構權限分類

若依Vue系統中的權限分為以下幾類:

1 菜單權限:使用者登入系統之後能看到哪些菜單

2 按鈕權限:使用者在一個頁面上能看到哪些按鈕,比如新增、删除等按鈕

3 接口權限:使用者帶着認證資訊請求後端接口,是否有權限通路,該接口和前端頁面上的按鈕一一對應

4 資料權限:使用者有權限通路後端某個接口,但是不同的使用者相同的接口相同的入參,根據權限大小不同,傳回的結果應當不一樣——權限大的能夠看到的資料更多。

2. 若依架構權限的依次介紹

1 菜單權限

這個比較好了解,擁有不同權限的使用者登入系統之後看到的菜單是不一樣的。(建立菜單、使用者配置設定菜單權限即可)

具體代碼1(後端):SysLoginController-------->getRouters()

select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
		from sys_menu m
			 left join sys_role_menu rm on m.menu_id = rm.menu_id
			 left join sys_user_role ur on rm.role_id = ur.role_id
			 left join sys_role ro on ur.role_id = ro.role_id
			 left join sys_user u on ur.user_id = u.user_id
		where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0
		order by m.parent_id, m.order_num

           

這是典型的使用者-角色-菜單模型。 菜單類型(M目錄 C菜單 F按鈕);

菜單狀态(0顯示 1隐藏)

前端會根據該接口傳回的資料渲染出不同的菜單。

資料的結構如下圖所示:(前端動态路由)

RuoYi-Vue————權限管理

具體代碼2(前端):ruoyi-ui\src\permission.js

permission.js檔案中設定了導航守衛,每次路由發生變化的時候就會觸發router.beforeEach的回調函數。
// 路由白名單
const whiteList = ['/login', '/auth-redirect', '/bind', '/register']

// 路由守衛
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) {
        // 判斷目前使用者是否已拉取完user_info資訊
        store.dispatch('GetInfo').then(() => {
          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()
    }
  }
})
router.afterEach(() => {
  NProgress.done()
})

           

注意if (store.getters.roles.length === 0) {這段邏輯,可以看出,如果不重新整理目前頁面,就算給使用者添加了新的菜單權限,使用者也看不到新的菜單。

2.按鈕權限

新增、删除、檢視、修改等按鈕,目前使用者是否能用

具體代碼1(後端):SysLoginController-------->getInfo() (前端拿到會存到vuex中)

// 将來這些資料,前端拿到會存到vuex中
    @GetMapping("getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        SysUser user = loginUser.getUser();
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // 權限集合
        Set<String> permissions = permissionService.getMenuPermission(user);
        AjaxResult ajax = AjaxResult.success();
        // 根據目前使用者,擷取目前使用者的所有user、roles、permissions資訊,并傳回
        ajax.put("user", user);
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }

           

具體代碼2(前端):ruoyi-ui\src\directive\permission

具體用法:

<el-button size="mini" 
            type="text" 
            icon="el-icon-edit" 
            @click="handleUpdate(scope.row)"
            v-hasPermi="['system:menu:edit']"   這裡就是按鈕權限      
          >修改
    </el-button>
    
    <el-button 
            size="mini" 
            type="text" 
            icon="el-icon-plus" 
            @click="handleAdd(scope.row)"
            v-hasPermi="['system:menu:add']"
          >新增
    </el-button>
           

按鈕權限 v-hasPermi 的具體實作:

export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const all_permission = "*:*:*";
    // 從vuex中擷取資料
    const permissions = store.getters && store.getters.permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value

      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })
      // 如果沒有 則移除removeChild
      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`請設定操作權限标簽值`)
    }
  }
}
           

3 接口權限:

接口權限和前端的按鈕權限一一對應。為的是防止使用者繞過按鈕直接請求後端接口擷取資料。在若依Vue系統中,是使用SpringSecurity的注解@PreAuthorize實作的。

具體代碼實作(這裡隻有後端)

@PreAuthorize("@ss.hasPermi('system:menu:edit')")
    @Log(title = "菜單管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
    {
        if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu)))
        {
            return AjaxResult.error("修改菜單'" + menu.getMenuName() + "'失敗,菜單名稱已存在");
        }
        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
        {
            return AjaxResult.error("修改菜單'" + menu.getMenuName() + "'失敗,位址必須以http(s)://開頭");
        }
        else if (menu.getMenuId().equals(menu.getParentId()))
        {
            return AjaxResult.error("修改菜單'" + menu.getMenuName() + "'失敗,上級菜單不能選擇自己");
        }
        menu.setUpdateBy(getUsername());
        return toAjax(menuService.updateMenu(menu));
    }
           
通過 @PreAuthorize("@ss.hasPermi(‘system:menu:edit’)")注解,實作了接口權限

接下來我們看 @PreAuthorize("@ss.hasPermi(‘system:menu:edit’)")

/**
     * 驗證使用者是否具備某權限
     * 
     * @param permission 權限字元串
     * @return 使用者是否具備某權限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

           

4 資料權限:

資料權限實作的關鍵在于com.ruoyi.framework.aspectj.DataScopeAspect類。該類是一個切面類,凡是加上com.ruoyi.common.annotation.DataScope注解的方法,在執行的時候都會被它攔截。

該切面定義了五種權限範圍

該切面的核心邏輯是“拼SQL”,方法執行之前,會給參數的一個params屬性添加一個dataScope鍵值對,key為"dataScope",值為AND (" + sqlString.substring(4) + ")"樣式的一段SQL,這段SQL會根據目前使用者所在的部門以及目前使用者角色的權限範圍發生變化。

簡單來說,這段代碼的邏輯就是使用者所在的部門權限越高,資料權限範圍越大,查出來的結果集将會越大。

DataScope注解分别加到了部門清單查詢、角色清單查詢、使用者清單查詢的接口上,很明顯,這幾個接口需要根據不同的人查出不同的結果。

以使用者清單查詢為例,執行sql為

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
		select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
		left join sys_dept d on u.dept_id = d.dept_id
		where u.del_flag = '0'
		<if test="userName != null and userName != ''">
			AND u.user_name like concat('%', #{userName}, '%')
		</if>
		<if test="status != null and status != ''">
			AND u.status = #{status}
		</if>
		<if test="phonenumber != null and phonenumber != ''">
			AND u.phonenumber like concat('%', #{phonenumber}, '%')
		</if>
		<if test="params.beginTime != null and params.beginTime != ''"><!-- 開始時間檢索 -->
			AND date_format(u.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
		</if>
		<if test="params.endTime != null and params.endTime != ''"><!-- 結束時間檢索 -->
			AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
		</if>
		<if test="deptId != null and deptId != 0">
			AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
		</if>
		<!-- 資料範圍過濾 -->
		${params.dataScope}
	</select>
           
實際上DataScopeAspect切面就隻幹了填充params的dataScope屬性這麼一件事情。(把之前拼好的sql扔這裡)

3. 若依架構重要接口執行流程

login

RuoYi-Vue————權限管理

getInfo

RuoYi-Vue————權限管理

getRouter

RuoYi-Vue————權限管理

是不是發現上述兩步不太嚴謹,對,我們可以自定義規則搞事情