风声话林语 發表於 2023-2-16 16:17:00

react 高效高质量搭建后台系统 系列 —— 前端权限

<blockquote>
<p>其他章节请看:</p>
<p>react 高效高质量搭建后台系统 系列</p>
</blockquote>
<h2 id="权限">权限</h2>
<p>本系列已近尾声,<code>权限</code>是后台系统必不可少的一部分,本篇首先分析spug项目中权限的实现,最后在将权限加入到我们的项目中来。</p>
<h3 id="spug-中权限的分析">spug 中权限的分析</h3>
<h4 id="权限示例">权限示例</h4>
<p>比如我要将<code>应用发布</code>模块的<code>查看</code>权限分给某用户(例如 <code>pjl</code>),可以这样操作:</p>
<ul>
<li>
<p>在角色管理中新建一角色(例如 <code>demo</code>),然后给该角色配置权限:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081130_highqualitybacksystem-permissions-01.png"></p>
</li>
<li>
<p>新建用户(<code>pjl</code>)并赋予其 demo 权限<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081138_highqualitybacksystem-permissions-02.png"></p>
</li>
<li>
<p>pjl 登录后就只能看到自己有权限的页面和操作:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081146_highqualitybacksystem-permissions-03.png"></p>
</li>
</ul>
<h4 id="入口">入口</h4>
<p>上述示例中,以 pjl 登录成功后返回如下数据:</p>
<pre><code class="language-javascript">{
    "data": {
      "id": 2,
      "access_token": "74b0fe67d09646ee9ca44fc48c6b457a",
      "nickname": "pjl",
      "is_supper": false,
      "has_real_ip": true,
      "permissions": [
            "deploy.app.view",
            "deploy.repository.view"
      ]
    },
    "error": ""
}
</code></pre>
<p>其中 permissions 表示该用户的权限。</p>
<p>vscode 搜索 <code>deploy.app.view</code> 有两处:</p>
<ul>
<li>发布配置(也是“应用管理”)页面入口 index.js。如果没有查看权限(<code>deploy.app.view</code>)就不展示该页面内容。</li>
</ul>
<pre><code class="language-javascript">// spug\src\pages\deploy\app\index.js

export default observer(function () {
return (
    &lt;AuthDiv auth="deploy.app.view"&gt;
      &lt;Breadcrumb&gt;
      &lt;Breadcrumb.Item&gt;首页&lt;/Breadcrumb.Item&gt;
      &lt;Breadcrumb.Item&gt;应用发布&lt;/Breadcrumb.Item&gt;
      &lt;Breadcrumb.Item&gt;应用管理&lt;/Breadcrumb.Item&gt;
      &lt;/Breadcrumb&gt;
      &lt;ComTable/&gt;
    &lt;/AuthDiv&gt;
);
})
</code></pre>
<p><em>Tip</em>:AuthDiv 用于控制页内组件的权限,里面通过 hasPermission 判断是否有对应的权限,如果没有,该组件内的子元素则不会显示。</p>
<pre><code class="language-javascript">export default function AuthDiv(props) {
let disabled = props.disabled === undefined ? false : props.disabled;
if (props.auth &amp;&amp; !hasPermission(props.auth)) {
    disabled = true;
}
return disabled ? null : &lt;div {...props}&gt;{props.children}&lt;/div&gt;
}
</code></pre>
<pre><code class="language-javascript">// 前端页面的权限判断(仅作为前端功能展示的控制,具体权限控制应在后端实现)
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('&amp;'))) {
      return true
    }
}
return false
}
</code></pre>
<ul>
<li>routes.js。里面定义的就是菜单(详见左侧导航)。<code>auth</code>指授权,这里定义的是模块和页面的查看权限。其中<code>deploy.app.view|deploy.repository.view|deploy.request.view</code>表示只要有其中一个权限,“应用发布”就会展示。</li>
</ul>
<pre><code class="language-javascript">// spug\src\routes.js
{
icon: &lt;FlagOutlined /&gt;, 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 },
]
},
</code></pre>
<h4 id="页面级的权限">页面级的权限</h4>
<p>现在我们已经知道页面级的权限解决思路:</p>
<ul>
<li>在定义菜单的地方(routes.js)通过 auth 定义<code>查看</code>(spug 中是 <code>xx.xx.view</code>)权限。如果没有 auth 则说明该页面任何人都可以访问</li>
<li>组装菜单时仅取出有权限的菜单页面</li>
</ul>
<pre><code class="language-javascript">// spug\src\layout\Sider.js
function makeMenu(menu) {
// 仅取出有权限的菜单
if (menu.auth &amp;&amp; !hasPermission(menu.auth)) return null;
if (!menu.title) return null;
return menu.child ? _makeSubMenu(menu) : _makeItem(menu)
}
</code></pre>
<p>如果直接通过 url 去访问没有权限的页面会如何:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081153_highqualitybacksystem-permissions-04.png"></p>
<p>直接 404。</p>
<p><code>为什么是 404?</code>因为路由数组(Routes)中只取了有权限的菜单页面。</p>
<pre><code class="language-javascript">// spug\src\layout\index.js
&lt;Switch&gt;
{/* 路由数组。里面每项类似这样:&lt;Route exact key={route.path} path='/home' component={HomeComponent}/&gt; */}
{Routes}
{/* 没有匹配则进入 NotFound */}
&lt;Route component={NotFound}/&gt;
&lt;/Switch&gt;
</code></pre>
<p>Routes 的内容请看路由初始化方法:</p>
<pre><code class="language-javascript">// 将 routes 中有权限的路由提取到 Routes 中
function initRoutes(Routes, routes) {
for (let route of routes) {
    if (route.component) {
      // 如果不需要权限,或有权限则放入 Routes
      if (!route.auth || hasPermission(route.auth)) {
      Routes.push(&lt;Route exact key={route.path} path={route.path} component={route.component}/&gt;)
      }
    } else if (route.child) {
      initRoutes(Routes, route.child)
    }
}
}
</code></pre>
<h4 id="页内级权限">页内级权限</h4>
<p>新增、删除等页内的权限如何实现的呢?我们把<code>发布配置</code>页内的<code>新建</code>权限放开,后端权限返回列表中将有: <code>deploy.app.add</code></p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081205_highqualitybacksystem-permissions-05.png"></p>
<p>vscode 只搜索到一处匹配<code>deploy.app.add</code>。</p>
<pre><code class="language-javascript">&lt;TableCard
      tKey="da"
      ...
      actions={[
      &lt;AuthButton
          auth="deploy.app.add"
          type="primary"
          icon={&lt;PlusOutlined/&gt;}
          onClick={() =&gt; store.showForm()}&gt;新建&lt;/AuthButton&gt;
      ]}
</code></pre>
<p>其中 AuthButton 和上文的 <code>AuthDiv</code> 一样,如果有权限则显示其中内容</p>
<pre><code class="language-javascript">// spug\src\components\AuthButton.js

export default function AuthButton(props) {
let disabled = props.disabled;
if (props.auth &amp;&amp; !hasPermission(props.auth)) {
    disabled = true;
}
return disabled ? null : &lt;Button {...props}&gt;{props.children}&lt;/Button&gt;
}
</code></pre>
<p>于是我们知道页内级的权限解决思路:</p>
<ul>
<li>在<code>功能权限设置</code>中定义对应操作权限的值,选中后把该值传递给后端</li>
</ul>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081217_highqualitybacksystem-permissions-06.png"></p>
<p>权限的值在 codes.js 中定义如下:</p>
<pre><code class="language-javascript">// 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: '新建版本' },
      ]
    },
    ...
]
},
</code></pre>
<ul>
<li>通过 AuthDiv、AuthButton等组件的 auth 匹配,如果没有权限则不展示该组件内容。</li>
</ul>
<h4 id="issuper">isSuper</h4>
<p>登录后返回权限中还有个字段 <code>is_supper</code>。数据格式就像这样:</p>
<pre><code class="language-javascript">{
    "data": {
      "id": 1,
      "access_token": "6a0cef69a7d64b6f8c755ff341266e76",
      "nickname": "管理员",
      "is_supper": true,
      "has_real_ip": true,
      "permissions": [ ]
    },
    "error": ""
}
</code></pre>
<p>is_supper 是 true,permissions 为空数组。猜测 is_supper 的作用有2个:</p>
<ul>
<li>一个用处(vscode 搜索 <code>isSuper</code>)直接返回有权限,无需其他判断。就像这样:</li>
</ul>
<pre><code class="language-javascript">// 前端页面的权限判断(仅作为前端功能展示的控制,具体权限控制应在后端实现)
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('&amp;'))) {
      return true
    }
}
return false
}
</code></pre>
<ul>
<li>一个用处(vscode 搜索 <code>is_supper</code>)是作为最大的权限再做一些页面上的操作。例如这里隐藏该片段:</li>
</ul>
<pre><code class="language-javascript">&lt;Form.Item hidden={store.record.is_supper} label="角色" style={{marginBottom: 0}}&gt;
&lt;Form.Item name="role_ids" style={{display: 'inline-block', width: '80%'}} extra="权限最大化原则,组合多个角色权限。"&gt;
    &lt;Select mode="multiple" placeholder="请选择"&gt;
      {roleStore.records.map(item =&gt; (
      &lt;Select.Option value={item.id} key={item.id}&gt;{item.name}&lt;/Select.Option&gt;
      ))}
    &lt;/Select&gt;
&lt;/Form.Item&gt;
&lt;Form.Item style={{display: 'inline-block', width: '20%', textAlign: 'right'}}&gt;
    &lt;Link to="/system/role"&gt;新建角色&lt;/Link&gt;
&lt;/Form.Item&gt;
&lt;/Form.Item&gt;
</code></pre>
<h3 id="myspug-中权限的实现">myspug 中权限的实现</h3>
<h4 id="需求">需求</h4>
<p>spug 中的权限整体思路:</p>
<ul>
<li>通过一个配置页面(<code>功能权限设置</code>弹框)将某些权限(页面级和页内级)赋予某角色,再让某用户属于该角色</li>
<li>用户登录成功后返回数据(<code>permissions</code>、<code>is_supper</code>字段)告诉前端该用户的权限</li>
<li>前端在组装菜单和路由时只取有权限的,就这样解决了页面级的权限问题</li>
<li>再通过封装的 AuthDiv、AuthButton 等组件嵌套页内级权限,例如新建,如果没有权限则不显示</li>
</ul>
<p>所以权限这里包含两条线:</p>
<ul>
<li>一条是用户通过配置页面,将权限赋予某用户,保存在后端数据库</li>
<li>另一条是用户登录后,根据后端返回的权限显示有权限查看的页面和操作</li>
</ul>
<p><code>需求</code>:用户没有以下三个权限(<code>报警联系人</code>、<code>报警联系组</code>、角色管理中的<code>新增</code>)。见下图3个红框:</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081224_highqualitybacksystem-permissions-07.png"></p>
<h4 id="实现效果">实现效果</h4>
<p>最终效果如下:</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081231_highqualitybacksystem-permissions-08.png"></p>
<h4 id="代码实现">代码实现</h4>
<h5 id="页面级权限配置">页面级权限配置</h5>
<p>在 routes.js 中定义每个菜单(也是路由)的权限:</p>
<pre><code class="language-javascript">export default [
// 无需权限
{ icon: &lt;DesktopOutlined /&gt;, title: '工作台', path: '/home', component: HomeIndex },
{
    icon: &lt;AlertOutlined /&gt;, 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: &lt;AlertOutlined /&gt;, title: '系统管理', auth: "system.role.view", child: [
      { title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole },
    ]
},
{ path: '/welcome/info', component: WelcomeInfo },
]
</code></pre>
<p><em>Tip</em>:例如<code>角色管理</code>页面需要<code>system.role.view</code>这个权限。命名任意,因为后端返回的权限是前端发送给的后端。</p>
<h5 id="haspermission">hasPermission</h5>
<p>更新权限判断逻辑。之前统统返回 true:</p>
<pre><code class="language-javascript">// 之前
export function hasPermission(strCode) {
    return true
}
</code></pre>
<p>现在看后端是否返回该权限:</p>
<pre><code class="language-javascript">// 前端页面的权限判断(仅作为前端功能展示的控制,具体权限控制应在后端实现)
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('&amp;'))) {
            return true
      }
    }
    return false
}

//数组包含关系判断
export function isSubArray(parent, child) {
    for (let item of child) {
      if (!parent.includes(item.trim())) {
            return false
      }
    }
    return true
}
</code></pre>
<h5 id="页内级权限组件">页内级权限组件</h5>
<p>定义两个组件(AuthButton、AuthDiv),并导出:</p>
<pre><code class="language-javascript">import React from 'react';
import { Button } from 'antd';
import { hasPermission } from '@/libs';

export default function AuthButton(props) {
let disabled = props.disabled;
if (props.auth &amp;&amp; !hasPermission(props.auth)) {
    disabled = true;
}
return disabled ? null : &lt;Button {...props}&gt;{props.children}&lt;/Button&gt;
}
</code></pre>
<pre><code class="language-javascript">import React from 'react';
import { hasPermission } from '@/libs';

export default function AuthDiv(props) {
    let disabled = props.disabled === undefined ? false : props.disabled;
    if (props.auth &amp;&amp; !hasPermission(props.auth)) {
      disabled = true;
    }
    return disabled ? null : &lt;div {...props}&gt;{props.children}&lt;/div&gt;
}
</code></pre>
<pre><code class="language-javascript">//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,
}
</code></pre>
<p>例如新建按钮就放入 AuthButton 组件中,而整个角色管理的入口模块就放入 AuthDiv 中。</p>
<h5 id="组装">组装</h5>
<ul>
<li>mock登录后的数据:</li>
</ul>
<pre><code class="language-javascript">// 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": ""
}
</code></pre>
<ul>
<li>角色管理入口页放入 AuthDiv:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\pages\system\role\index.js

export default function () {
return (
    &lt;AuthDiv auth="system.role.view"&gt;
      &lt;ComTable /&gt;
    &lt;/AuthDiv&gt;
)
}
</code></pre>
<ul>
<li>新增按钮放入 AuthButton:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\pages\system\role\Table.js
&lt;TableCard
      rowKey="id"
      title="角色列表"
      actions={[
      &lt;AuthButton type="primary" icon={&lt;PlusOutlined/&gt;} auth="system.role.add" &gt;新增&lt;/AuthButton&gt;
      ]}
</code></pre>
<h3 id="扩展">扩展</h3>
<h4 id="spug-中权限的缺点">spug 中权限的缺点</h4>
<p>spug 中权限虽然简单,其中一个缺点是每开发一个新模块,就得更新权限配置页面(即<code>功能权限设置</code>)和路由(routes.js)配置:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230216081130_highqualitybacksystem-permissions-01.png"></p>
<p>如果将其改为可配置,将减小这部分的工作。</p>
<blockquote>
<p>其他章节请看:</p>
<p>react 高效高质量搭建后台系统 系列</p>
</blockquote>


</div>
<div id="MySignature" role="contentinfo">
    <section class="m-reprintStatement" style="white-space:normal;/* 防止break-all与nowrap矛盾 */word-break:break-all;">
    作者:彭加李<br>
    出处:https://www.cnblogs.com/pengjiali/p/17127181.html<br>
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
</section><br><br>
来源:https://www.cnblogs.com/pengjiali/p/17127181.html
頁: [1]
查看完整版本: react 高效高质量搭建后台系统 系列 —— 前端权限