基于React的简单权限设计

前端进行权限控制只是为了用户体验,对应的角色渲染对应的视图,真正的安全保障在后端。

前言

毕业之初,工作的主要内容便是开发一个后台管理系统,当时存在的一个现象是:

用户若记住了某个 url,直接浏览器输入,不论该用户是否拥有访问该页面的权限,均能进入页面。

若页面初始化时(componentDidMount)进行接口请求,后端会返回 403 的 HTTP 状态码,同时前端封装的request.js会对非业务异常进行相关处理,遇见 403,就重定向到无权限页面。

若是页面初始化时不存在前后端交互,那就要等用户触发某些操作(比如表单提交)后才会触发上述流程。

可以看到,安全保障是后端兜底的,那前端能做些什么呢?

  1. 明确告知用户没有权限,避免用户误以为自己拥有该权限而进行操作(即使无法操作成功),直接跳转至无权限页面;
  2. 拦截明确无权的请求,比如某些需要权限才能进行的操作入口(按钮 or 导航等)不对无权用户展示,其实本点包含上一点。

最近也在看Ant Design Pro的权限相关处理,有必要进行一次总结。

需要注意的是,本文虽然基于Ant Design Pro的权限设计思路,但并不是完全对其源码的解读(可能更偏向于 v1 的涉及思路,不涉及 umi)。

如果有错误以及理解偏差请轻捶并指正,谢谢。

模块级别的权限处理

假设存在以下关系:

角色 role 权限枚举值 authority 逻辑
普通用户 user 不展示
管理员 admin 展示“进入管理后台”按钮

某页面上存在一个文案为“进入管理后台”的按钮,只对管理员展示,让我们实现一下。

简单实现

// currentAuthority 为当前用户权限枚举值

const AdminBtn = ({ currentAuthority }) => {
  if ("admin" === currentAuthority) {
    return <button>进入管理后台</button>;
  }
  return null;
};

好吧,简单至极。

权限控制就是if else,实现功能并不复杂,大不了每个页面|模块|按钮涉及到的处理都写一遍判断就是了,总能实现需求的。

不过,现在只是一个页面中的一个按钮而已,我们还会碰到许多“某(几)个页面存在某个 xxx,只对 xxx(或/以及 xxx) 展示”的场景。

所以,还能做的更好一些。

下面来封装一个最基本的权限管理组件Authorized

组件封装-Authorized

期望调用形式如下:

<Authorized
  currentAuthority={currentAuthority}
  authority={"admin"}
  noMatch={null}
>
  <button>进入管理后台</button>
</Authorized>

api 如下:

参数 说明 类型 默认值
children 正常渲染的元素,权限判断通过时展示 ReactNode
currentAuthority 当前权限 string
authority 准入权限 string/string[]
noMatch 未通过权限判断时展示 ReactNode

currentAuthority这个属性没有必要每次调用都手动传递一遍,此处假设用户信息是通过 redux 获取并存放在全局 store 中。

注意:我们当然也可以将用户信息挂在 window 下或者 localStorage 中,但很重要的一点是,绝大部分场景我们都是通过接口异步获取的数据,这点至关重要。如果是 html 托管在后端或是 ssr的情况下,服务端直接注入了用户信息,那真是再好不过了。

新建src/components/Authorized/Authorized.jsx实现如下:

import { connect } from "react-redux";

function Authorized(props) {
  const { children, userInfo, authority, noMatch } = props;
  const { currentAuthority } = userInfo || {};
  if (!authority) return children;
  const _authority = Array.isArray(authority) ? authority : [authority];
  if (_authority.includes(currentAuthority)) return children;
  return noMatch;
}

export default connect(store => ({ userInfo: store.common.userInfo }))(
  Authorized
);

现在我们无需手动传递currentAuthority

<Authorized authority={"admin"} noMatch={null}>
  <button>进入管理后台</button>
</Authorized>

✨ 很好,我们现在迈出了第一步。

Ant Design Pro中,对于currentAuthority(当前权限)与authority(准入权限)的匹配功能,定义了一个checkPermissions方法,提供了各种形式的匹配,本文只讨论authority为数组(多个准入权限)或字符串(单个准入权限),currentAuthority为字符串(当前角色只有一种权限)的情况。

页面级别的权限处理

页面就是放在Route组件下的模块。

知道这一点后,我们很轻松的可以写出如下代码:

新建src/router/index.jsx,当用户角色与路由不匹配时,渲染Redirect组件用于重定向。

import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import NormalPage from "@/views/NormalPage"; /* 公开页面 */
import UserPage from "@/views/UserPage"; /* 普通用户和管理员均可访问的页面*/
import AdminPage from "@/views/AdminPage"; /* 管理员才可访问的页面*/
import Authorized from "@/components/Authorized";

// Layout就是一个布局组件,写一些公用头部底部啥的

function Router() {
  <BrowserRouter>
    <Layout>
      <Switch>
        <Route exact path="/" component={NormalPage} />

        <Authorized
          authority={["admin", "user"]}
          noMatch={
            <Route
              path="/user-page"
              render={() => <Redirect to={{ pathname: "/login" }} />}
            />
          }
        >
          <Route path="/user-page" component={UserPage} />
        </Authorized>

        <Authorized
          authority={"admin"}
          noMatch={
            <Route
              path="/admin-page"
              render={() => <Redirect to={{ pathname: "/403" }} />}
            />
          }
        >
          <Route path="/admin-page" component={AdminPage} />
        </Authorized>
      </Switch>
    </Layout>
  </BrowserRouter>;
}

export default Router;

这段代码是不 work 的,因为当前权限信息是通过接口异步获取的,此时Authorized组件获取不到当前权限(currentAuthority),倘若直接通过 url 访问/user-page/admin-page,不论用户身份是否符合,请求结果未回来,都会被重定向到/login/403,这个问题后面再谈。

先优化一下我们的代码。

抽离路由配置

路由配置相关 jsx 内容太多了,页面数量过多就不好维护了,可读性也大大降低,我们可以将路由配置抽离出来。

新建src/router/router.config.js,专门用于存放路由相关配置信息。

import NormalPage from "@/views/NormalPage";
import UserPage from "@/views/UserPage";
import AdminPage from "@/views/AdminPage";

export default [
  {
    exact: true,
    path: "/",
    component: NormalPage
  },
  {
    path: "/user-page",
    component: UserPage,
    authority: ["user", "admin"],
    redirectPath: "/login"
  },
  {
    path: "/admin-page",
    component: AdminPage,
    authority: ["admin"],
    redirectPath: "/403"
  }
];

组件封装-AuthorizedRoute

接下来基于Authorized组件对Route组件进行二次封装。

新建src/components/Authorized/AuthorizedRoute.jsx

实现如下:

import React from "react";
import { Route } from "react-router-dom";
import Authorized from "./Authorized";

function AuthorizedRoute({
  component: Component,
  render,
  authority,
  redirectPath,
  ...rest
}) {
  return (
    <Authorized
      authority={authority}
      noMatch={
        <Route
          {...rest}
          render={() => <Redirect to={{ pathname: redirectPath }} />}
        />
      }
    >
      <Route
        {...rest}
        render={props => (Component ? <Component {...props} /> : render(props))}
      />
    </Authorized>
  );
}

export default AuthorizedRoute;

优化后

现在重写我们的 Router 组件。

import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import AuthorizedRoute from "@/components/AuthorizedRoute";
import routeConfig from "./router.config.js";

function Router() {
  <BrowserRouter>
    <Layout>
      <Switch>
        {routeConfig.map(rc => {
          const { path, component, authority, redirectPath, ...rest } = rc;
          return (
            <AuthorizedRoute
              key={path}
              path={path}
              component={component}
              authority={authority}
              redirectPath={redirectPath}
              {...rest}
            />
          );
        })}
      </Switch>
    </Layout>
  </BrowserRouter>;
}

export default Router;

心情舒畅了许多。

可是还留着一个问题呢——由于用户权限信息是异步获取的,在权限信息数据返回之前,AuthorizedRoute组件就将用户推到了redirectPath

其实Ant Design Pro v4 版本就有存在这个问题,相较于 v2 的@/pages/Authorized组件从localStorage中获取权限信息,v4 改为从 redux 中获取(redux 中的数据则是通过接口获取),和本文比较类似。具体可见此次 PR

异步获取权限

解决思路很简单:保证相关权限组件挂载时,redux 中已经存在用户权限信息。换句话说,接口数据返回后,再进行相关渲染。

我们可以在 Layout 中进行用户信息的获取,数据获取完毕后渲染children

结语

Ant Design Pro从 v2 开始底层基于 umi实现,通过路由配置的 Routes 属性,结合@/pages/Authorized组件(该组件基于@/utils/Authorized组件——@/components/Authorized的二次封装,注入currentAuthority(当前权限))实现主要流程。 同时,权限信息存放于localStorage,通过@/utils/authority.js提供的工具方法进行权限 get 以及 set

仔细看了下@/components/Authorized文件下的内容,发现还提供了AuthorizedRoute组件,但是并未在代码中使用(取而代之的是@/pages/Authorized组件),翻了 issue 才了解到,v1 没有基于umi的时候,是基于AuthorizedRoute进行路由权限管理的,升级了之后,AuthorizedRoute则并没有用于路由权限管理。

涉及到的相关文件比较多(components/pages/utils),v4 的文档又有些缺失,看源码的话,若没有理清版本之间差异,着实会有些费力。

本文在权限信息获取上,通过接口异步获取,存放至 redux(和 v4 版本有些类似,见@/pages/Authorized以及@/layouts/SecurityLayout)。

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

推荐阅读更多精彩内容

  • 包含 react基础 react传值 react路由 和redux管理 和react-redux reactDom...
    栀璃鸢年_49a3阅读 1,100评论 0 1
  • 前言 umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。这个库入门的门槛比较高,umi是这个样...
    侬姝沁儿阅读 29,158评论 9 20
  • 水木年华的一首歌词“爱是什么?我不知道,我不懂自己,谁能懂自己?”已经是奔三的年纪,按照古人的说法,应该是三十而立...
    天刀笑剑钝阅读 119评论 0 0
  • 今天TTT课试讲,以前上去都会非常紧张,感觉太爽了,从来没有放松过,上去讲之前都会越来越紧张,直到自己上去,紧张到...
    lee左阅读 146评论 0 0
  • 牛牛的好朋友丁丁不是学校里的朋友。丁丁是《丁丁历险记》的主人公,牛牛最喜欢的人。牛牛最喜欢《丁丁历险记》中的月球探...
    大漠飞雪111阅读 165评论 0 0