react 高效高質量搭建後臺系統 系列 —— 前端許可權

2023-02-16 18:00:56

其他章節請看:

react 高效高質量搭建後臺系統 系列

許可權

本系列已近尾聲,許可權是後臺系統必不可少的一部分,本篇首先分析spug專案中許可權的實現,最後在將許可權加入到我們的專案中來。

spug 中許可權的分析

許可權範例

比如我要將應用釋出模組的檢視許可權分給某使用者(例如 pjl),可以這樣操作:

  • 在角色管理中新建一角色(例如 demo),然後給該角色設定許可權:

  • 新建使用者(pjl)並賦予其 demo 許可權

  • pjl 登入後就只能看到自己有許可權的頁面和操作:

入口

上述範例中,以 pjl 登入成功後返回如下資料:

{
    "data": {
        "id": 2, 
        "access_token": "74b0fe67d09646ee9ca44fc48c6b457a", 
        "nickname": "pjl", 
        "is_supper": false, 
        "has_real_ip": true, 
        "permissions": [
            "deploy.app.view", 
            "deploy.repository.view"
        ]
    }, 
    "error": ""
}

其中 permissions 表示該使用者的許可權。

vscode 搜尋 deploy.app.view 有兩處:

  • 釋出設定(也是「應用管理」)頁面入口 index.js。如果沒有檢視許可權(deploy.app.view)就不展示該頁面內容。
// spug\src\pages\deploy\app\index.js

export default observer(function () {
  return (
    <AuthDiv auth="deploy.app.view">
      <Breadcrumb>
        <Breadcrumb.Item>首頁</Breadcrumb.Item>
        <Breadcrumb.Item>應用釋出</Breadcrumb.Item>
        <Breadcrumb.Item>應用管理</Breadcrumb.Item>
      </Breadcrumb>
      <ComTable/>
    </AuthDiv>
  );
})

Tip:AuthDiv 用於控制頁內元件的許可權,裡面通過 hasPermission 判斷是否有對應的許可權,如果沒有,該元件內的子元素則不會顯示。

export default function AuthDiv(props) {
  let disabled = props.disabled === undefined ? false : props.disabled;
  if (props.auth && !hasPermission(props.auth)) {
    disabled = true;
  }
  return disabled ? null : <div {...props}>{props.children}</div>
}
// 前端頁面的許可權判斷(僅作為前端功能展示的控制,具體許可權控制應在後端實現)
export function hasPermission(strCode) {
  const {isSuper, permissions} = Permission;
  if (!strCode || isSuper) return true;
  for (let or_item of strCode.split('|')) {
    if (isSubArray(permissions, or_item.split('&'))) {
      return true
    }
  }
  return false
}
  • routes.js。裡面定義的就是選單(詳見左側導航)。auth指授權,這裡定義的是模組和頁面的檢視許可權。其中deploy.app.view|deploy.repository.view|deploy.request.view表示只要有其中一個許可權,「應用釋出」就會展示。
// spug\src\routes.js
{
  icon: <FlagOutlined />, title: '應用釋出', auth: 'deploy.app.view|deploy.repository.view|deploy.request.view', child: [
    { title: '應用管理', auth: 'deploy.app.view', path: '/deploy/app', component: DeployApp },
    { title: '構建倉庫', auth: 'deploy.repository.view', path: '/deploy/repository', component: DeployRepository },
    { title: '釋出申請', auth: 'deploy.request.view', path: '/deploy/request', component: DeployRequest },
  ]
},

頁面級的許可權

現在我們已經知道頁面級的許可權解決思路:

  • 在定義選單的地方(routes.js)通過 auth 定義檢視(spug 中是 xx.xx.view)許可權。如果沒有 auth 則說明該頁面任何人都可以存取
  • 組裝選單時僅取出有許可權的選單頁面
// spug\src\layout\Sider.js
function makeMenu(menu) {
  // 僅取出有許可權的選單
  if (menu.auth && !hasPermission(menu.auth)) return null;
  if (!menu.title) return null;
  return menu.child ? _makeSubMenu(menu) : _makeItem(menu)
}

如果直接通過 url 去存取沒有許可權的頁面會如何:

直接 404。

為什麼是 404?因為路由陣列(Routes)中只取了有許可權的選單頁面。

// spug\src\layout\index.js
<Switch>
  {/* 路由陣列。裡面每項類似這樣:<Route exact key={route.path} path='/home' component={HomeComponent}/> */}
  {Routes}
  {/* 沒有匹配則進入 NotFound */}
  <Route component={NotFound}/>
</Switch>

Routes 的內容請看路由初始化方法:

// 將 routes 中有許可權的路由提取到 Routes 中
function initRoutes(Routes, routes) {
  for (let route of routes) {
    if (route.component) {
      // 如果不需要許可權,或有許可權則放入 Routes
      if (!route.auth || hasPermission(route.auth)) {
        Routes.push(<Route exact key={route.path} path={route.path} component={route.component}/>)
      }
    } else if (route.child) {
      initRoutes(Routes, route.child)
    }
  }
}

頁內級許可權

新增、刪除等頁內的許可權如何實現的呢?我們把釋出設定頁內的新建許可權放開,後端許可權返回列表中將有: deploy.app.add

vscode 只搜尋到一處匹配deploy.app.add

<TableCard
      tKey="da"
      ...
      actions={[
        <AuthButton
          auth="deploy.app.add"
          type="primary"
          icon={<PlusOutlined/>}
          onClick={() => store.showForm()}>新建</AuthButton>
      ]}

其中 AuthButton 和上文的 AuthDiv 一樣,如果有許可權則顯示其中內容

// spug\src\components\AuthButton.js

export default function AuthButton(props) {
  let disabled = props.disabled;
  if (props.auth && !hasPermission(props.auth)) {
    disabled = true;
  }
  return disabled ? null : <Button {...props}>{props.children}</Button>
}

於是我們知道頁內級的許可權解決思路:

  • 功能許可權設定中定義對應操作許可權的值,選中後把該值傳遞給後端

許可權的值在 codes.js 中定義如下:

// codes.js
{
  key: 'deploy',
  label: '應用釋出',
  pages: [
    {
      key: 'app',
      label: '應用管理',
      perms: [
        { key: 'view', label: '檢視應用' },
        { key: 'add', label: '新建應用' },
        { key: 'edit', label: '編輯應用' },
        { key: 'del', label: '刪除應用' },
        { key: 'config', label: '檢視設定' },
      ]
    },
    {
      key: 'repository',
      label: '構建倉庫',
      perms: [
        { key: 'view', label: '檢視構建' },
        { key: 'add', label: '新建版本' },
      ]
    },
    ...
  ]
},
  • 通過 AuthDiv、AuthButton等元件的 auth 匹配,如果沒有許可權則不展示該元件內容。

isSuper

登入後返回許可權中還有個欄位 is_supper。資料格式就像這樣:

{
    "data": {
        "id": 1, 
        "access_token": "6a0cef69a7d64b6f8c755ff341266e76", 
        "nickname": "管理員", 
        "is_supper": true, 
        "has_real_ip": true, 
        "permissions": [ ]
    }, 
    "error": ""
}

is_supper 是 true,permissions 為空陣列。猜測 is_supper 的作用有2個:

  • 一個用處(vscode 搜尋 isSuper)直接返回有許可權,無需其他判斷。就像這樣:
// 前端頁面的許可權判斷(僅作為前端功能展示的控制,具體許可權控制應在後端實現)
export function hasPermission(strCode) {
  const {isSuper, permissions} = Permission;
  if (!strCode || isSuper) return true;
  for (let or_item of strCode.split('|')) {
    if (isSubArray(permissions, or_item.split('&'))) {
      return true
    }
  }
  return false
}
  • 一個用處(vscode 搜尋 is_supper)是作為最大的許可權再做一些頁面上的操作。例如這裡隱藏該片段:
<Form.Item hidden={store.record.is_supper} label="角色" style={{marginBottom: 0}}>
  <Form.Item name="role_ids" style={{display: 'inline-block', width: '80%'}} extra="許可權最大化原則,組合多個角色許可權。">
    <Select mode="multiple" placeholder="請選擇">
      {roleStore.records.map(item => (
        <Select.Option value={item.id} key={item.id}>{item.name}</Select.Option>
      ))}
    </Select>
  </Form.Item>
  <Form.Item style={{display: 'inline-block', width: '20%', textAlign: 'right'}}>
    <Link to="/system/role">新建角色</Link>
  </Form.Item>
</Form.Item>

myspug 中許可權的實現

需求

spug 中的許可權整體思路:

  • 通過一個設定頁面(功能許可權設定彈框)將某些許可權(頁面級和頁內級)賦予某角色,再讓某使用者屬於該角色
  • 使用者登入成功後返回資料(permissionsis_supper欄位)告訴前端該使用者的許可權
  • 前端在組裝選單和路由時只取有許可權的,就這樣解決了頁面級的許可權問題
  • 再通過封裝的 AuthDiv、AuthButton 等元件巢狀頁內級許可權,例如新建,如果沒有許可權則不顯示

所以許可權這裡包含兩條線:

  • 一條是使用者通過設定頁面,將許可權賦予某使用者,儲存在後端資料庫
  • 另一條是使用者登入後,根據後端返回的許可權顯示有許可權檢視的頁面和操作

需求:使用者沒有以下三個許可權(報警聯絡人報警聯絡組、角色管理中的新增)。見下圖3個紅框:

實現效果

最終效果如下:

程式碼實現

頁面級許可權設定

在 routes.js 中定義每個選單(也是路由)的許可權:

export default [
  // 無需許可權
  { icon: <DesktopOutlined />, title: '工作臺', path: '/home', component: HomeIndex },
  {
    icon: <AlertOutlined />, title: '報警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
      { title: '報警歷史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmCenter },
      { title: '報警聯絡人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmCenter },
      { title: '報警聯絡組', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmCenter },
    ]
  },
  {
    icon: <AlertOutlined />, title: '系統管理', auth: "system.role.view", child: [
      { title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole },
    ]
  },
  { path: '/welcome/info', component: WelcomeInfo },
]

Tip:例如角色管理頁面需要system.role.view這個許可權。命名任意,因為後端返回的許可權是前端傳送給的後端。

hasPermission

更新許可權判斷邏輯。之前統統返回 true:

// 之前
export function hasPermission(strCode) {
    return true
}

現在看後端是否返回該許可權:

// 前端頁面的許可權判斷(僅作為前端功能展示的控制,具體許可權控制應在後端實現)
export function hasPermission(strCode) {
    const { isSuper, permissions } = Permission;

    if (!strCode || isSuper) return true;
    for (let or_item of strCode.split('|')) {
        if (isSubArray(permissions, or_item.split('&'))) {
            return true
        }
    }
    return false
}

//  陣列包含關係判斷
export function isSubArray(parent, child) {
    for (let item of child) {
        if (!parent.includes(item.trim())) {
            return false
        }
    }
    return true
}
頁內級許可權元件

定義兩個元件(AuthButton、AuthDiv),並匯出:

import React from 'react';
import { Button } from 'antd';
import { hasPermission } from '@/libs';

export default function AuthButton(props) {
  let disabled = props.disabled;
  if (props.auth && !hasPermission(props.auth)) {
    disabled = true;
  }
  return disabled ? null : <Button {...props}>{props.children}</Button>
}
import React from 'react';
import { hasPermission } from '@/libs';

export default function AuthDiv(props) {
    let disabled = props.disabled === undefined ? false : props.disabled;
    if (props.auth && !hasPermission(props.auth)) {
        disabled = true;
    }
    return disabled ? null : <div {...props}>{props.children}</div>
}
//  myspug\src\compoments\index.js
import NotFound from './NotFound';
import TableCard from './TableCard';
import AuthButton from './AuthButton'
import AuthDiv from './AuthDiv'

export {
    NotFound,
    TableCard,
    AuthButton,
    AuthDiv,
}

例如新建按鈕就放入 AuthButton 元件中,而整個角色管理的入口模組就放入 AuthDiv 中。

組裝
  • mock登入後的資料:
// mock/index.js
{
	"data": {
		"id": 2,
		"access_token": "74b0fe67d09646ee9ca44fc48c6b457a",
		"nickname": "pjl",
		"is_supper": false,
		"has_real_ip": true,
		"permissions": ["system.role.view", "alarm.alarm.view"]
	},
	"error": ""
}
  • 角色管理入口頁放入 AuthDiv:
// myspug\src\pages\system\role\index.js

export default function () {
  return (
    <AuthDiv auth="system.role.view">
      <ComTable />
    </AuthDiv>
  )
}
  • 新增按鈕放入 AuthButton:
// myspug\src\pages\system\role\Table.js
<TableCard
      rowKey="id"
      title="角色列表"
      actions={[
        <AuthButton type="primary" icon={<PlusOutlined/>} auth="system.role.add" >新增</AuthButton>
      ]}

擴充套件

spug 中許可權的缺點

spug 中許可權雖然簡單,其中一個缺點是每開發一個新模組,就得更新許可權設定頁面(即功能許可權設定)和路由(routes.js)設定:

如果將其改為可設定,將減小這部分的工作。

其他章節請看:

react 高效高質量搭建後臺系統 系列