antd pro V4 鉴权流程解析

antd pro 的鉴权流程乍一看还是挺复杂的,涉及到了很多文件,但是实际上搞清楚了几个核心调用之后整个流程也就很明朗了,这篇文章就从头开始,介绍一下 antd pro v4 的鉴权流程。

我整理了一份 antd pro v4 鉴权流程图,配合流程图阅读本文体验更佳。

起点 BasicLayout

整个鉴权的起点位于 src\layouts\BasicLayout.jsx 中,作为基础框架,这个组件包含了 导航菜单渲染主要内容渲染 两大职责。而这两块也都有鉴权的参与,我们先从导航菜单开始讲。

导航菜单中的鉴权

导航菜单主要是由 ProLayout 的 menuDataRender 函数进行渲染。这个函数的入参就是我们在 config/routes.js 里边配置的路由(只有 BasicLayout 下面的子路由 ),umi 会自动把这些路由注入给 ProLayout。

/**
 * 渲染 - 递归侧边栏菜单
 */
const menuDataRender = (menuList) =>
    menuList.map((item) => {
        const localItem = {
            ...item,
            children: item.children ? menuDataRender(item.children) : undefined,
        };
        // 注意下面这一行
        return Authorized.check(item.authority, localItem, null);
    });

可以看到,这个渲染函数会递归的调用 Authorized.check 这个方法,而这个方法会接受三个参数,按照顺序依次是 对应路由项所需的权限、鉴权成功后的渲染内容、鉴权失败后的渲染内容,这个方法会拿 传入的权限(item.authority)和当前用户的权限进行比较,并根据结果决定渲染哪个内容,可以看到这里第三个参数传入了一个 null,所以如果登录的用户没有权限的话对应的菜单项就不会显示了。

访问页面的鉴权

如果用户不通过侧边栏的导航菜单,而是直接在地址栏里打开没有权限的页面呢?为了阻止这种非法访问操作,antd pro 在 ProLayout 里添加了一层 Authorized 组件来拦截用户的访问,如下:

<ProLayout
    // ...
>
    <Authorized authority={authorized.authority} noMatch={noMatch}>
        {children}
    </Authorized>
</ProLayout>
    );

Authorized 组件和上面介绍的 Authorized.check 方法是差不多的,他会接受一个 authority 作为当前打开页面(就是代码中的 {children})的所需权限,并和当前用户的权限进行比对,如果鉴权失败的话就会渲染 noMatch 元素。

我们可以看到给 authority 参数传递的是一个叫做 authorized.authority 的变量,这个变量来自于同文件中的一个 useMemo:

const authorized = useMemo(() => {
    return (
        getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
            authority: undefined,
        }
    );
}, [location.pathname]);

这个操作也很简单,如果地址栏中的路径名(location.pathname)变化了,那么就重新从路由列表中找到当前路径名对应路由的所需权限。antd pro 就是通过这种方式来保持 Authorized 组件获取的永远是当前组件的所需权限。

问题解决:在地址栏中访问没有权限的页面可以直接打开

官方在 v4 版本中确实曾经有过这个问题,如果你也遇到了,将上面的 getAuthorityFromRouter 替换为如下函数即可解决:

import pathRegexp from 'path-to-regexp';

export const getAuthorityFromRouter = (router, pathname) => {
    const authority = router.find(
        ({ routes, path = '/', target = '_self' }) =>
            (path && target !== '_blank' && pathRegexp(path).exec(pathname)) ||
            (routes && getAuthorityFromRouter(routes, pathname)),
    );
    if (authority) return authority;
    return undefined;
};

关于该问题的后续见 这里

核心组件 Authorized

在 BasicLayout.jsx 中我们可以发现,目前所需的鉴权功能都是来自于 @/utils/Authorized 的 Authorized 组件,接下来我们就来介绍一下它,在对应的文件里可以找到如下几行代码:

let Authorized = RenderAuthorize(getAuthority());

const reloadAuthorized = () => {
    Authorized = RenderAuthorize(getAuthority());
};

非常简单,大致就是在全局通过 RenderAuthorize 方法新建一个组件实例,并且提供了一个 reload 方法用于更新这个全局实例。

RenderAuthorize 我们稍后再谈,可以看到后面还用到了一个 getAuthority 方法,这个方法来自于 src\utils\authority.js,功能也很简单,就是 获取当前用户的所有权限。antd pro 是将其临时存放到了 localStorage 里,这不太安全,所以建议在自己的项目里将其改为从线上获取。

authority.js 里还提供了一个 setAuthority 方法,这个方法用于 设置用户的权限信息。这个方法会在 登录成功时调用,并且设置之后还会调用刚才提到的 reloadAuthorized 方法来重新加载 Authorized 组件。

了解到了用户所有权限是怎么来的,接下来我们就来看一下 Authorized 组件到底是怎么生成的,首先我们找到 RenderAuthorize 的来源 src\components\Authorized\index.jsx。可以看到里边的核心代码也很简单:

Authorized.check = check;
const RenderAuthorize = renderAuthorize(Authorized);

首先把我们用到的 check 方法附加到 Authorized,然后用...一个小写的 renderAuthorize 接受了一个 Authorized,生成了一个大写的 RenderAuthorize??

也就是说,我把一个 Authorized 传递给 renderAuthorize,它返回一个 RenderAuthorize,而这个函数会返回另一个 Authorized?

是的你没有看错,我当初也被这个操作迷惑到了。事实上,这里 renderAuthorize 接受的 Authorized 和我们在文章开头了解到的那个 Authorized 就是 同一个实例!并且这个 renderAuthorize 函数做的也不是什么生成或者渲染工作,它只是创建了一个延迟函数,然后 把当前的用户所有权限转存到了一个全局变量上。你可以在 src\components\Authorized\renderAuthorize.js 你看到它的真面貌:

/**
 * 当前的用户权限,全局唯一
 */
let CURRENT = 'NULL';

/**
 * 获取 Authorized 实例
 * 就是把 Authorized 传递出去,同时搞了个延迟函数接受并储存当前的用户权限
 */
const renderAuthorize = (Authorized) => (currentAuthority) => {
    if (currentAuthority) {
        // 是函数就将其返回值当作权限
        if (typeof currentAuthority === 'function') {
            CURRENT = currentAuthority();
        }

        // 是数组或者字符串就直接将其作为权限
        if (
            Object.prototype.toString.call(currentAuthority) === '[object String]' ||
            Array.isArray(currentAuthority)
        ) {
            CURRENT = currentAuthority;
        }
    } else {
        CURRENT = 'NULL';
    }

    return Authorized;
};

是不是有点晕了,不用担心,我们只需要知道这个稍显复杂的函数只做了一件事,就是把当前用户的权限(currentAuthority)保存到了全局变量(CURRENT)里就行了。

接下来我们无视这个函数,继续去看 Authorized 组件到底是什么,打开 src\components\Authorized\Authorized.jsx

const Authorized = ({
    children,
    authority,
    noMatch = (
        <Result
            status="403"
            title="403"
            subTitle="Sorry, you are not authorized to access this page."
        />
    ),
}) => {
    const childrenRender = typeof children === 'undefined' ? null : children;
    const dom = check(authority, childrenRender, noMatch);
    return <>{dom}</>;
};

非常的简单,其实就是我们在上文“导航菜单渲染”里用到的 check 函数的组件封装。而 check 函数就更简单了,如下,它额外去拿了我们刚才转存的 CURRENT(当前的用户权限),然后把这四个参数一股脑的塞给了同文件中的 checkPermissions 函数:

import { CURRENT } from './renderAuthorize';

/**
 * 使用指定鉴权方式检查是否允许访问对应组件
 *
 * @param {string|array|Promise|function} authority 鉴权方式,会根据不同的类型进行不同的鉴权方法
 * @param {ReactElement} target 鉴权成功后渲染的组件
 * @param {ReactElement} Exception 鉴权失败后渲染的组件
 * @returns 要渲染的组件,target 和 Exception 二选一
 */
function check(authority, target, Exception) {
    return checkPermissions(authority, CURRENT, target, Exception);
}

而至于 checkPermissions 函数,你别看它很长,其实做的操作也很简单,就是比较第一个参数(组件所需的权限)和第二个参数(用户有的权限),如果比较通过(鉴权成功)了,就返回第三个参数(鉴权成功对应的组件),比较不通过的话就返回第四个参数(鉴权失败对应的组件)。

它之所以那么长,是因为 组件所需权限(参数一)支持 stringarrayfunction 以及 promise,而用户拥有的权限(参数二)也支持 stringarray,为了抹平类型不同带来的差异性。这个函数里包含了上述所有可能性对应的鉴权操作。

其中值得注意的是,当组件所需权限是 promise 或者 是一个返回 promise 的函数时,它会把相关内容都转交给一个异步加载组件 PromiseRender 进行渲染。这个组件也很简单,如果 promise reslove 了就渲染鉴权成功的组件,reject 了就渲染鉴权失败的组件,并且在 pending 时渲染一个转圈圈的加载动画。

至此整个鉴权流程就结束了。

总结

antd pro 的鉴权流程其实也很简单,其中的核心函数就是 checkPermissions。在导航菜单渲染时会调用它进行检查,如果不通过就返回 null 从而不显示无权限的菜单。而页面组件渲染则是使用了封装了 checkPermissions 的 Authorized 组件,这时鉴权失败就会返回我们熟悉的 403 页面。

而在用户的权限信息发生变化时,会触发副作用函数 renderAuthorize 来将新的用户权限存到 CURRENT 变量里,checkPermissions 会读取 CURRENT 来和组件所需的权限进行对比。

而对比完成后,如果所需权限和用户权限不涉及异步的话,checkPermissions 会直接返回对应的内容,涉及异步的话会交由 PromiseRender 进行加载后判断。

参考

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

推荐阅读更多精彩内容