ant design pro v2 - 权限控制

Posted 耳东蜗牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ant design pro v2 - 权限控制相关的知识,希望对你有一定的参考价值。

同步语雀地址https://www.yuque.com/chenzilong/nigxcx/qtv5n3
github地址https://github.com/rodchen-king/ant-design-pro-v2/tree/permission-branch
 

后台管理平台内部权限大部分涉及到到两种方式:资源权限 & 数据权限
 

1. 基本介绍

资源权限:菜单导航栏 & 页面 & 按钮 资源可见权限。
数据权限:对于页面上的数据操作,同一个人同一个页面不同的数据可能存在不同的数据操作权限。

权限纬度
角色纬度:大部分的情况为:用户 => 角色 => 权限
用户纬度:用户 => 权限

表现形式
基础表现形式还是树结构的展现形式,因为对应的菜单-页面-按钮是一个树的从主干到节点的数据流向。


2. 权限数据录入与展示

采用树结构进行处理。唯一需要处理的是父子节点的联动关系处理。
 

3. 权限数据控制

3.1 用户资源权限流程图


3.2 前端权限控制

前端控制权限也是分为两部分,菜单页面 与 按钮。因为前端权限控制的实现,会因为后台接口形式有所影响,但是大体方向是相同。还是会分为这两块内容。这里对于权限是使用多接口查询权限,初始登录查询页面权限,点击业务页面,查询对应业务页面的资源code。
 

3.2.1 菜单权限

菜单权限控制需要了解两个概念:

  • 一个是可见的菜单页面        : 左侧dom节点
  • 一个是可访问的菜单页面     : 系统当中路由这一块

这里说的意思是:我们所说的菜单权限控制,大多只是停留在菜单是否可见,但是系统路由的页面可见和页面上的菜单是否可见是两回事情。假设系统路由/path1可见,尽管页面上的没有/path1对应的菜单显示。我们直接在浏览器输入对应的path1,还是可以访问到对应的页面。这是因为系统路由那一块其实我们是没有去处理的。

了解了这个之后,我们需要做菜单页面权限的时候就需要去考虑两块,并且是对应的。
 

3.2.1.1 路由权限 【代码地址  demo地址

这里是有两种做法:

这里还是先用第一种做法来做:因为这里用第一种做了之后,菜单可见权限自动适配好了。会省去我们很多事情。

a. 路由文件,定义菜单页面权限。并且将exception以及404的路由添加notInAut标志,这个标志说明:这两个路由不走权限校验。同理的还有 /user。

export default [
  // user
  
    path: '/user',
    component: '../layouts/UserLayout',
    routes: [
       path: '/user', redirect: '/user/login' ,
       path: '/user/login', component: './User/Login' ,
       path: '/user/register', component: './User/Register' ,
       path: '/user/register-result', component: './User/RegisterResult' ,
    ],
  ,
  // app
  
    path: '/',
    component: '../layouts/BasicLayout',
    Routes: ['src/pages/Authorized'],
    authority: ['admin', 'user'],
    routes: [
      // dashboard
       path: '/', redirect: '/list/table-list' ,
      // forms
      
        path: '/form',
        icon: 'form',
        name: 'form',
        code: 'form_menu',
        routes: [
          
            path: '/form/basic-form',
            code: 'form_basicForm_page',
            name: 'basicform',
            component: './Forms/BasicForm',
          ,
        ],
      ,
      // list
      
        path: '/list',
        icon: 'table',
        name: 'list',
        code: 'list_menu',
        routes: [
          
            path: '/list/table-list',
            name: 'searchtable',
            code: 'list_tableList_page',
            component: './List/TableList',
          ,
        ],
      ,
      
        path: '/profile',
        name: 'profile',
        icon: 'profile',
        code: 'profile_menu',
        routes: [
          // profile
          
            path: '/profile/basic',
            name: 'basic',
            code: 'profile_basic_page',
            component: './Profile/BasicProfile',
          ,
          
            path: '/profile/advanced',
            name: 'advanced',
            code: 'profile_advanced_page',
            authority: ['admin'],
            component: './Profile/AdvancedProfile',
          ,
        ],
      ,
      
        name: 'exception',
        icon: 'warning',
        notInAut: true,
        hideInMenu: true,
        path: '/exception',
        routes: [
          // exception
          
            path: '/exception/403',
            name: 'not-permission',
            component: './Exception/403',
          ,
          
            path: '/exception/404',
            name: 'not-find',
            component: './Exception/404',
          ,
          
            path: '/exception/500',
            name: 'server-error',
            component: './Exception/500',
          ,
          
            path: '/exception/trigger',
            name: 'trigger',
            hideInMenu: true,
            component: './Exception/TriggerException',
          ,
        ],
      ,
      
        notInAut: true,
        component: '404',
      ,
    ],
  ,
];


b. 修改app.js 文件,加载路由

export const dva = 
  config: 
    onError(err) 
      err.preventDefault();
    ,
  ,
;

let authRoutes = null;

function ergodicRoutes(routes, authKey, authority) 
  routes.forEach(element => 
    if (element.path === authKey) 
      Object.assign(element.authority, authority || []);
     else if (element.routes) 
      ergodicRoutes(element.routes, authKey, authority);
    
    return element;
  );


function customerErgodicRoutes(routes) 
  const menuAutArray = (localStorage.getItem('routerAutArray') || '').split(',');

  routes.forEach(element => 
    // 没有path的情况下不需要走逻辑检查
    // path 为 /user 不需要走逻辑检查
    if (element.path === '/user' || !element.path) 
      return element;
    

    // notInAut 为true的情况下不需要走逻辑检查
    if (!element.notInAut) 
      if (menuAutArray.indexOf(element.code) >= 0 || element.path === '/') 
        if (element.routes) 
          // eslint-disable-next-line no-param-reassign
          element.routes = customerErgodicRoutes(element.routes);

          // eslint-disable-next-line no-param-reassign
          element.routes = element.routes.filter(item => !item.isNeedDelete);
        
       else 
        // eslint-disable-next-line no-param-reassign
        element.isNeedDelete = true;
      
    

    /**
     * 后台接口返回子节点的情况,父节点需要溯源处理
     */
    // notInAut 为true的情况下不需要走逻辑检查
    // if (!element.notInAut) 
    //   if (element.routes) 
    //     // eslint-disable-next-line no-param-reassign
    //     element.routes = customerErgodicRoutes(element.routes);

    //     // eslint-disable-next-line no-param-reassign
    //     if (element.routes.filter(item => item.isNeedSave && !item.hideInMenu).length) 
    //       // eslint-disable-next-line no-param-reassign
    //       element.routes = element.routes.filter(item => item.isNeedSave);
    //       if (element.routes.length) 
    //         // eslint-disable-next-line no-param-reassign
    //         element.isNeedSave = true;
    //       
    //     
    //    else if (menuAutArray.indexOf(element.code) >= 0) 
    //     // eslint-disable-next-line no-param-reassign
    //     element.isNeedSave = true;
    //   
    //  else 
    //   // eslint-disable-next-line no-param-reassign
    //   element.isNeedSave = true;
    // 

    return element;
  );

  return routes;


export function patchRoutes(routes) 
  Object.keys(authRoutes).map(authKey =>
    ergodicRoutes(routes, authKey, authRoutes[authKey].authority),
  );

  customerErgodicRoutes(routes);

  /**
   * 后台接口返回子节点的情况,父节点需要溯源处理
   */
  window.g_routes = routes.filter(item => !item.isNeedDelete);

  /**
   * 后台接口返回子节点的情况,父节点需要溯源处理
   */
  // window.g_routes = routes.filter(item => item.isNeedSave);


export function render(oldRender) 
  authRoutes = '';
  oldRender();


c. 修改login.js,获取路由当中的code便利获取到,进行查询权限

import  routerRedux  from 'dva/router';
import  stringify  from 'qs';
import  fakeAccountLogin, getFakeCaptcha  from '@/services/api';
import  getAuthorityMenu  from '@/services/authority';
import  setAuthority  from '@/utils/authority';
import  getPageQuery  from '@/utils/utils';
import  reloadAuthorized  from '@/utils/Authorized';
import routes from '../../config/router.config';

export default 
  namespace: 'login',

  state: 
    status: undefined,
  ,

  effects: 
    *login( payload ,  call, put ) 
      const response = yield call(fakeAccountLogin, payload);
      yield put(
        type: 'changeLoginStatus',
        payload: response,
      );
      // Login successfully
      if (response.status === 'ok') 
        // 这里的数据通过接口返回菜单页面的权限是什么

        const codeArray = [];
        // eslint-disable-next-line no-inner-declarations
        function ergodicRoutes(routesParam) 
          routesParam.forEach(element => 
            if (element.code) 
              codeArray.push(element.code);
            
            if (element.routes) 
              ergodicRoutes(element.routes);
            
          );
        

        ergodicRoutes(routes);
        const authMenuArray = yield call(getAuthorityMenu, codeArray.join(','));
        localStorage.setItem('routerAutArray', authMenuArray.join(','));

        reloadAuthorized();
        const urlParams = new URL(window.location.href);
        const params = getPageQuery();
        let  redirect  = params;
        if (redirect) 
          const redirectUrlParams = new URL(redirect);
          if (redirectUrlParams.origin === urlParams.origin) 
            redirect = redirect.substr(urlParams.origin.length);
            if (redirect.match(/^\\/.*#/)) 
              redirect = redirect.substr(redirect.indexOf('#') + 1);
            
           else 
            window.location.href = redirect;
            return;
          
        
        // yield put(routerRedux.replace(redirect || '/'));

        // 这里之所以用页面跳转,因为路由的重新设置需要页面重新刷新才可以生效
        window.location.href = redirect || '/';
      
    ,

    *getCaptcha( payload ,  call ) 
      yield call(getFakeCaptcha, payload);
    ,

    *logout(_,  put ) 
      yield put(
        type: 'changeLoginStatus',
        payload: 
          status: false,
          currentAuthority: 'guest',
        ,
      );
      reloadAuthorized();
      yield put(
        routerRedux.push(
          pathname: '/user/login',
          search: stringify(
            redirect: window.location.href,
          ),
        ),
      );
    ,
  ,

  reducers: 
    changeLoginStatus(state,  payload ) 
      setAuthority(payload.currentAuthority);
      return 
        ...state,
        status: payload.status,
        type: payload.type,
      ;
    ,
  ,
;


d. 添加service

import request from '@/utils/request';

// 查询菜单权限
export async function getAuthorityMenu(codes) 
  return request(`/api/authority/menu?resCodes=$codes`);


// 查询页面按钮权限
export async function getAuthority(params) 
  return request(`/api/authority?codes=$params`);



3.2.1.2 菜单可见权限

参照上面的方式,这里的菜单可见权限不用做其他的操作。
 

3.2.2 按钮权限 【代码地址  demo地址

按钮权限上就涉及到两块,资源权限和数据权限。数据获取的方式不同,代码逻辑上会稍微有点不同。核心是业务组件内部的code,在加载的时候就自行累加,然后在页面加载完成的时候,发送请求。拿到数据之后,自行进行权限校验。尽量减少业务页面代码的复杂度。
 

资源权限逻辑介绍:

  1. PageHeaderWrapper包含的业务页面存在按钮权限
  2. 按钮权限通过AuthorizedButton包含处理,需要添加code。但是业务页面因为是单独页面发送当前页面code集合去查询权限code,然后在AuthorizedButton进行权限逻辑判断。
  3. 所以AuthorizedButtoncomponentWillMount生命周期进行当前业务页面的code累加。累加完成之后,通过PageHeaderWrappercomponentDidMount生命周期函数发送权限请求,拿到权限code,通过公有globalAuthority model读取数据进行权限逻辑判断。
  4. 对于业务页面的调用参考readme进行使用。因为对于弹出框内部的code,在业务列表页面渲染的时候,组件还未加载,所以通过extencode提前将code累加起来进行查询权限。
     

数据权限介绍:

  1. 涉及数据权限,则直接将对应的数据规则放进AuthorizedButton内部进行判断,需要传入的数据则直接通过props传入即可。因为数据权限的规则不同,这里就没有举例子。
  2. 需要注意的逻辑是资源权限和数据权限是串行的,先判断资源权限,然后判断数据权限。


a. 添加公用authority model

/* eslint-disable no-unused-vars */
/* eslint-disable no-prototype-builtins */
import  getAuthority  from '@/services/authority';

export default 
  namespace: 'globalAuthority',

  state: 
    hasAuthorityCodeArray: [], // 获取当前具有权限的资源code
    pageCodeArray: [], // 用来存储当前页面存在的资源code
  ,

  effects: 
    /**
     * 获取当前页面的权限控制
     */
    *getAuthorityForPage( payload ,  put, call, select ) 
      // 这里的资源code都是自己加载的
      const pageCodeArray = yield select(state => state.globalAuthority.pageCodeArray);
      const response = yield call(getAuthority, pageCodeArray);

      if (pageCodeArray.length) 
        yield put(
          type: 'save',
          payload: 
            hasAuthorityCodeArray: response,
          ,
        );
      
    ,

    *plusCode( payload ,  put, select ) 
      // 组件累加当前页面的code,用来发送请求返回对应的权限code
      const  codeArray = []  = payload;
      const pageCodeArray = yield select(state => state.globalAuthority.pageCodeArray);

      yield put(
        type: 'save',
        payload: 
          pageCodeArray: pageCodeArray.concat(codeArray),
        ,
      );
    ,

    // eslint-disable-next-line no-unused-vars
    *resetAuthorityForPage( payload ,  put, call ) 
      yield put(
        type: 'save',
        payload: 
          hasAuthorityCodeArray: [],
          pageCodeArray: [],
        ,
      );
    ,
  ,

  reducers: 
    save(state,  payload ) 
      return 
        ...state,
        ...payload,
      ;
    ,
  ,
;


b. 修改PageHeaderWrapper文件【因为所有的业务页面都是这个组件的子节点】

import React,  PureComponent  from 'react';
import  FormattedMessage  from 'umi/locale';
import Link from 'umi/link';
import PageHeader from '@/components/PageHeader';
import  connect  from 'dva';
import MenuContext from '@/layouts/MenuContext';
import  Spin  from 'antd';
import GridContent from './GridContent';
import styles from './index.less';

class PageHeaderWrapper extends PureComponent 
  componentDidMount() 
    const  dispatch  = this.props;
    dispatch(
      type: 'globalAuthority/getAuthorityForPage', // 发送请求获取当前页面的权限code
    );
  

  componentWillUnmount() 
    const  dispatch  = this.props;
    dispatch(
      type: 'globalAuthority/resetAuthorityForPage',
    );
  

  render() 
    const  children, contentWidth, wrapperClassName, top, loading, ...restProps  = this.props;

    return (
      <Spin spinning=loading>
        <div style= margin: '-24px -24px 0'  className=wrapperClassName>
          top
          <MenuContext.Consumer>
            value => (
              <PageHeader
                wide=contentWidth === 'Fixed'
                home=<FormattedMessage id="menu.home" defaultMessage="Home" />
                ...value
                key="pageheader"
                ...restProps
                linkElement=Link
                itemRender=item => 
                  if (item.locale) 
                    return <FormattedMessage id=item.locale defaultMessage=item.title />;
                  
                  return item.title;
                
              />
            )
          </MenuContext.Consumer>
          children ? (
            <div className=styles.content>
              <GridContent>children</GridContent>
            </div>
          ) : null
        </div>
      </Spin>
    );
  


export default connect(( setting, globalAuthority, loading ) => (
  contentWidth: setting.contentWidth,
  globalAuthority,
  loading: loading.models.globalAuthority,
))(PageHeaderWrapper);


c. 添加AuthorizedButton公共组件

import React,  Component  from 'react';
import PropTypes from 'prop-types';
import  connect  from 'dva';

@connect(( globalAuthority ) => (
  globalAuthority,
))
class AuthorizedButton extends Component 
  static contextTypes = 
    isMobile: PropTypes.bool,
  ;

  componentWillMount() 
    // extendcode 扩展表格中的code还没有出现的情况
    const 
      dispatch,
      code,
      extendCode = [],
      globalAuthority:  pageCodeArray ,
     = this.props;

    let codeArray = [];

    if (code) 
      codeArray.push(code);
    

    if (extendCode && extendCode.length) 
      codeArray = codeArray.concat(extendCode);
    

    // code已经存在,证明是页面数据渲染之后或者弹出框的按钮资源,不需要走dva了
    if (pageCodeArray.indexOf(code) >= 0) 
      return;
    

    dispatch(
      type: 'globalAuthority/plusCode',
      payload: 
        codeArray,
      ,
    );
  

  checkAuthority = code => 
    const 
      globalAuthority:  hasAuthorityCodeArray ,
     = this.props;

    return hasAuthorityCodeArray.indexOf(code) >= 0; // 资源权限
  ;

  render() 
    const  children, code  = this.props;

    return (
      <span style= display: this.checkAuthority(code) ? 'inline' : 'none' >children</span>
    );
  


export default AuthorizedButton;


d. 添加AuthorizedButton readme文件

权限组件,通过比对现有权限与准入权限,决定相关元素的展示。

<br>

## 核心思想
***

<br>

页面按钮资源在页面初始化过程的时候,计算当前页面的资源按钮,在业务页面加载完成之后,发送请求到后台访问资源。

<br>

## 使用方式

<br>

#### 1. 普通用法 【页面资源初始化的时候,按钮已经加载完成】

<br>

```
<AuthoriedButton code="10002">
  <Button icon="plus" type="primary" onClick=() => this.handleModalVisible(true)>
    编辑
  </Button>
</AuthoriedButton>
```

<br>

| 参数      | 说明                                      | 类型         | 默认值 |
|----------|------------------------------------------|-------------|-------|
| code    | 页面资源code           | string  | - |

<br><br><br>

#### 2. 扩展用法 【页面资源初始化的时候,按钮资源未加载完成】

<br>

<font style="color: red">此时我们需要扩展组件去代发按钮资源还没加载的数据</font>

<br>

```
<AuthoriedButton code="10001" extendCode=["10005"]>
  <Button icon="plus" type="primary" onClick=() => this.handleModalVisible(true)>
    新建
  </Button>
</AuthoriedButton>
```

<br>

| 参数      | 说明                                      | 类型         | 默认值 |
|----------|------------------------------------------|-------------|-------|
| code    | 页面资源code           | string  | - |
| extendCode    | 需要代发请求的扩展code,例如表格行内的按钮           | string[]  | - |

<br>

<font style="color: red">假如当前页面没有初始化已经加载完成的按钮,则直接使用下面方式</font>

<br>

```
<AuthoriedButton extendCode=["10006"] />

```

<br>

| 参数      | 说明                                      | 类型         | 默认值 |
|----------|------------------------------------------|-------------|-------|
| extendCode    | 需要代发请求的扩展code,例如表格行内的按钮           | string[]  | - |

 

以上是关于ant design pro v2 - 权限控制的主要内容,如果未能解决你的问题,请参考以下文章

013-ant design pro advanced 错误处理

Ant Design Pro安装学习

005-ant design pro 新增业务组件

ant design pro上传图片到后端

移除antd pro中的 路由的国际化

ant-design-pro 开发 基于react 框架涉及到技术你的本地环境需要安装 yarnnode 和 git。我们的技术栈基于 ES2015+ReactUmiJSdvag2 和 a