权限管理(React)

系统权限

本文主要是总结前段时间项目中权限管理这块开发设计,包括:资源(路由级)权限、操作(按钮级)权限,以及登录后用户级别的权限分配。当然还有数据级权限,这个暂时没有加入,最后会简单提出个解决思路。

背景

公司需要做一款产品,里面需要有一个平台用来类似手机APP似的房子不同的子产品入口(类快捷图标),各子产品间实现单点登录,创建不同账户级别,可以分配产品权限,产品资源权限,产品操作级权限。

image

本产品,最后权限做了双重控制,前后端都控制, 本文只从前端角度进行总结。

  • 单点登录采用CAS
  • 前端技术采用React技术栈:使用了一个自建的简易脚手架FireLeaf-React-Scaffold

登录(产品)权限

此产品因为涉及到数据库表,用户管理,数据调度等一系列子产品功能,因此设计上用户分为不同级别,拥有不同产品权限,不同的账户登录,也就展示的不同。

账户所拥有的产品权限信息,登录后后台将会返回数组形式,每项包含一些信息,至于这些产品信息管理,也在后台系统中进行统一管理配置,之后将会在资源权限提及。

[{
  id: "1",
  isLeaf:0,
  name:"用户管理中心",
  nodeId:"node0",
  pOrgCode:"",
  uri:"",
  url:"/uc"
}]

其中,主要是url来进行跳转,这里有个问题:url里的路径有时是同一域名下的产品,也可以是一些以前的产品路径,这就需要进行url判断

handleALink = (href) => {
  const regex = /(https?:\/\/)?(\w+\.?)+(\/[a-zA-Z0-9\\?%=_\-\\+\\/]+)?/gi;
  const url = href.replace(regex, function (match, capture) {  
    if (capture) {  
      return match;
    }  
    else {  
      return 'http://' + match;  
    }  
  });

  const a = document.createElement('a');
  a.href = url;
  a.target = '_blank';
  a.click();
  window.URL.revokeObjectURL(url);
}

当然,有人会问,如果直接进入一个产品地址,如何判断登录呢?
我们前后端约定好一个未登录code码 401

...
xhr.success = (res, options) => {
  if (typeof res !== 'object') {
    message.error( apiUrl + ': response data should be JSON');
    return;
  }
  switch (res.code) {
    case 200:
      options.success && options.success(res);
      break;
    case 401:
      auth.destroy();
      message.error('请登录!');
      toLogin(res.url);
      break;
    default:
      message.error(res.message || 'unknown error');
  }
};
...

import auth from 'src/utils/auth';

function toLogin(url) {
  auth.destroy();
  localStorage.clear();
  
  const toHref = url;
  window.location.href = toHref;
}

export default toLogin;

其实前端也将登录后的用户信息存入了localstorage, 退出登录后将会销毁,这也能进行登录验证,但是不是很准确;当然这里其实还得进行路由权限验证,这下面将会讲到。

资源(路由)权限

登录后,进入某产品后,将会获得此产品所拥有的资源权限树形数据,将用来渲染一些导航栏,或者跳转导航。

路由权限设计有些考虑的问题:

  • 路由主要分为这几类:未登录可访问、登录显示导航中(显性)、登录未显示在导航中(隐性)
  • 未登录后需要跳转到未登录访问首页,当然此产品没有,直接跳到单点登录页
  • 已登录后,跳转无权限路由,将转到404页

后台系统资源管理设计

image

image

资源管理采用树形结构,同级叶子可以进行拖拽调换位置展示导航菜单,每级叶子均可以添加叶子,删除修改。叶子的信息这里有些特有的设计:

  • 节点类别:
    • 外链项目:产品项目属于需要外来的,挂载在平台下,webUrl将要填写完整地址
    • 项目根:产品项目
    • 资源根:导航级的根
    • 资源叶子:导航级叶子(显性),如果此类型下还有叶子,那将属于隐性路由
  • web类别:菜单级(显性),内容级(隐性)
  • 资源操作类型:每个路由级界面下的操作权限配置

对于此颗资源树的数据操作保存的管理开发,其实当产品越来越多,树将会越来越繁杂,层级节点将会越来越多,需要将树进行产品级的保存及展示优化

登录后,对显示菜单进行渲染后,要对访问的路由进行访问权限审核检查:

// react-router
<Router
    history={browserHistory}
  >
    <Route path={PName}
      onEnter={(...args) => {
      requireAuth(...args);
    }}
    component={App} 
    breadcrumbName="/">
     ....
    </Route>
</Router>

// 用户登录验证, 路由权限检查
function requireAuth(nextState, replace) {
  const path = nextState.location.pathname;

  if(auth.isLoginIn()) {
    if(!checkRouter(path)) {
      replace({
        pathname: PName + '/404',
        state: {
          referrer: path
        }
      });
    }
  }  
}

// 入口app.js中检测

componentWillReceiveProps(nextProps) {
  const { location } = nextProps;
  // URL 发生改变的时候检查是否有权限访问
  if (location.pathname === '/' || location.pathname !== this.props.location.pathname) {
    checkRouter(routers);
  }
}

/**
 * 路由检测
 * @param {url_auth} obj
 * @param {path} str 
 */
import {PName} from 'utils/config';
import auth from 'utils/auth';

function isNumber(obj) {
  var num = obj - 0;
  return typeof num === 'number' && !isNaN(num);  
}
 
const checkRouter = (path) => {
  const resource = auth.getResource();
  const {urlAuth} = resource;
  const urlList = [].concat(urlAuth);
  urlList.push(PName + '/404');
  
  const arr = urlList.filter(item => {
    const arr = path.split('/');
    if(isNumber(arr[arr.length - 1])) {
      arr.pop();

      return item.indexOf(arr.join('/')) > -1;
    }
    else {
      return path == item;
    }
  });

  return !!arr.length;
};

export default checkRouter;

操作(按钮级)权限

每个路由界面级均有操作权限配置信息,因此需要对所有页面的按钮级操作进行权限配置,主要通过显隐操作来控制

操作权限管理界面

image

操作权限主要是设计了一个webkey配置,方便前端的操作权限的检测。操作权限是进行统一管理的,路径资源管理下可以进行操作权限的勾选配置。
操作权限由于涉及到按钮级,也就是组件级,不能在每个页面单独配置,那样需求改动,将会陷入深坑。我采用的HOC高阶组件的封装套路:

/**
 * 权限 高阶组件
 * @param {reactNode} ComposedComponent 需要权限控制的组件
 * @param {string} path 页面pathname
 * @param {string} webKey 操作web key
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import auth from 'utils/auth';
import tools from 'utils/tools';

const {getNodeByKeys} = tools;

const wrapAuth = (ComposedComponent, path) => class WrapComponent extends Component {
  // 构造
  constructor(props) {
    super(props);
    this.state = {
      webKey: props.webKey
    };
  }

  checkBtnAuth(webKey) {
    const {navList} = auth.getResource();
    const keys = [];
    keys.push(path);
    let bol = true;

    const nodes = getNodeByKeys(navList, keys, 'url');

    if(nodes.length) {
      const indexNode = nodes[0];
      const types = indexNode.type;

      if(types) {
        const type = types.find(item => item.webKey == webKey);
        if(!type) {
          bol = false;
        }
      }
    }

    return bol;
  }

  static propTypes = {
    webKey: PropTypes.string.isRequired, // 按钮级的权限webKey
  };

  render() {
    const {webKey, ...others} = this.props;
    if (this.checkBtnAuth(webKey)) {
      return <ComposedComponent  { ...others} />;
    } else {
      return null;
    }
  }
};

export default wrapAuth;

界面中使用也是很简单:

// 操作权限
const authPath = PName + '/pm/pmTable';
const AuthButton = wrapAuth(Button, authPath);
...
<AuthButton webKey={AUTH_BTN_KEY.ADD} icon='plus' type="primary" onClick={this.handleAdd}>新建</AuthButton>

这样采用HOC进行封装,可以进行一些别的需要扩展:加入操作动画,改变样式等。

数据(接口)权限

不同的用户登录以后,对数据范围的权限是有限制的,那些能够访问,那些不能访问在产品设计的是就已经定义好,当访问一个当前登录用户无权访问的 API 或者数据的时候,API 响应中会返回对应的 code, 这个 code 是提前就前后的约定好的值。
这部分权限需要在xhr api层调用接口时进行数据权限的判断

总结

总结一下,其实前端在做权限控制的时候,依赖于后端 API 返回的配置信息,所以在权限设计,路由设计,数据结构设计的时候,前后端一定要约定好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 140,386评论 1 295
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,319评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 92,325评论 0 206
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,558评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,200评论 1 249
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,496评论 1 167
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,191评论 2 265
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 28,971评论 0 161
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,688评论 6 223
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,330评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,098评论 2 211
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,412评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,116评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 26,937评论 2 206
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,314评论 3 200
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,558评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,879评论 0 162
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,275评论 2 226
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,386评论 2 228

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,360评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 133,983评论 18 139
  • 这个男人,你伤心了,他不明白你为什么伤心。你生气了,他不明白你为什么生气。你难过了他不知道怎么才能让你开心。也许他...
    672584dcb0dd阅读 190评论 0 0
  • 儿子回家前把新家各个地方都用手机照了下来,又录了下来。 回到老家了,儿子又把大房子和小房子的屋里又都照了下来,又给...
    时间再过的慢一点吧阅读 87评论 1 1
  • 今天有段时间有些茫然,不知所措。就顺手翻开了书柜的一本书《人性的弱点》,只是读了前面的序和作者写到的给读者的几条阅...
    许小小丽阅读 259评论 1 3