前端進行權限控制只是爲了用戶體驗,對應的角色渲染對應的視圖,真正的安全保障在後端。html
畢業之初,工做的主要內容即是開發一個後臺管理系統,當時存在的一個現象是:前端
用戶若記住了某個 url,直接瀏覽器輸入,不論該用戶是否擁有訪問該頁面的權限,均能進入頁面。react
若頁面初始化時(componentDidMount
)進行接口請求,後端會返回 403 的 HTTP 狀態碼,同時前端封裝的request.js
會對非業務異常進行相關處理,碰見 403,就重定向到無權限頁面。git
如果頁面初始化時不存在先後端交互,那就要等用戶觸發某些操做(好比表單提交)後纔會觸發上述流程。github
能夠看到,安全保障是後端兜底的,那前端能作些什麼呢?redux
最近也在看Ant Design Pro
的權限相關處理,有必要進行一次總結。後端
須要注意的是,本文雖然基於Ant Design Pro
的權限設計思路,但並非徹底對其源碼的解讀(可能更偏向於 v1 的涉及思路,不涉及 umi)。api
若是有錯誤以及理解誤差請輕捶並指正,謝謝。數組
假設存在如下關係:瀏覽器
角色 role | 權限枚舉值 authority | 邏輯 |
---|---|---|
普通用戶 | user | 不展現 |
管理員 | admin | 展現「進入管理後臺」按鈕 |
某頁面上存在一個文案爲「進入管理後臺」的按鈕,只對管理員展現,讓咱們實現一下。
// currentAuthority 爲當前用戶權限枚舉值
const AdminBtn = ({ currentAuthority }) => {
if ("admin" === currentAuthority) {
return <button>進入管理後臺</button>;
}
return null;
};
複製代碼
好吧,簡單至極。
權限控制就是if else
,實現功能並不複雜,大不了每一個頁面|模塊|按鈕涉及到的處理都寫一遍判斷就是了,總能實現需求的。
不過,如今只是一個頁面中的一個按鈕而已,咱們還會碰到許多「某(幾)個頁面存在某個 xxx,只對 xxx(或/以及 xxx) 展現」的場景。
因此,還能作的更好一些。
下面來封裝一個最基本的權限管理組件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"
}
];
複製代碼
接下來基於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
)。