這是我參與更文挑戰的第4天,活動詳情查看: 更文挑戰javascript
後臺管理平臺內部權限大部分涉及到到兩種方式: 資源權限 & 數據權限前端
說明:下面的代碼是react + ant design pro的例子。java
權限緯度react
表現形式git
採用樹結構進行處理。惟一須要處理的是父子節點的聯動關係處理。這裏由於不一樣的公司或者系統可能對於這部分的數據錄入方式不一樣,因此久不貼圖了。github
前端控制權限也是分爲兩部分,菜單頁面 與 按鈕。由於前端權限控制的實現,會由於後臺接口形式有所影響,可是大致方向是相同。仍是會分爲這兩塊內容。這裏對於權限是使用多接口查詢權限,初始登陸查詢頁面權限,點擊業務頁面,查詢對應業務頁面的資源code。
web
菜單權限控制須要瞭解兩個概念:redux
這裏說的意思是:咱們所說的菜單權限控制,大多隻是停留在菜單是否可見,可是系統路由的頁面可見和頁面上的菜單是否可見是兩回事情。假設系統路由/path1可見,儘管頁面上的沒有/path1對應的菜單顯示。咱們直接在瀏覽器輸入對應的path1,仍是能夠訪問到對應的頁面。這是由於系統路由那一塊其實咱們是沒有去處理的。 api
瞭解了這個以後,咱們須要作菜單頁面權限的時候就須要去考慮兩塊,而且是對應的。
數組
這裏是有兩種作法:
這裏仍是先用第一種作法來作:由於這裏用第一種作了以後,菜單可見權限自動適配好了。會省去咱們不少事情。
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) {
element.routes = customerErgodicRoutes(element.routes);
element.routes = element.routes.filter(item => !item.isNeedDelete);
}
} else {
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}`);
}
複製代碼
參照上面的方式,這裏的菜單可見權限不用作其餘的操做。
按鈕權限上就涉及到兩塊,資源權限和數據權限。數據獲取的方式不一樣,代碼邏輯上會稍微有點不一樣。核心是業務組件內部的code,在加載的時候就自行累加,而後在頁面加載完成的時候,發送請求。拿到數據以後,自行進行權限校驗。儘可能減小業務頁面代碼的複雜度。
資源權限邏輯介紹:
數據權限介紹:
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文件
github.com/rodchen-kin…
背景:頁面上有須要控制跳轉連接的權限,有權限則能夠跳轉,沒有權限則不能跳轉。
a.公共model添加新的state:codeAuthorityObject
經過redux-devtool,查看到codeAuthorityObject的狀態值爲:key:code值,value的值爲true/false。 true表明,有權限,false表明無權限。主要用於開發人員本身作相關處理。
b.須要控制的按鈕code,經過其餘方式擴展進行code計算,發送請求獲取權限
c.獲取數據進行數據控制
數據權限是對於業務組件內部表格組件的數據進行的數據操做權限。列表數據可能歸屬於不一樣的數據類型,因此具備不一樣的數據操做權限。對於批量操做則須要判斷選擇的數據是否都具備操做權限,而後顯示是否能夠批量操做,若是有一個沒有操做權限,都不能進行操做。
場景:
好比在商品列表中,每條商品記錄後面的「操做」一欄下用三個按鈕:【編輯】、【上架/下架】、【刪除】,而對於某一個用戶,他能夠查看全部的商品,但對於某些品牌他能夠【上架/下架】但不能【編輯】,則前端須要控制到每個商品後面的按鈕的可用狀態。
好比用戶A對於某一條業務數據(id=1999)有編輯權限,則這條記錄上的【編輯】按鈕對他來講是可見的(前提是他首先要有【編輯】這個按鈕的資源權限),但對於另外一條記錄(id=1899)是沒有【編輯】權限,則這條記錄上的【編輯】按鈕對他來講是不可見的。
每一個數據操做的按鈕上加一個屬性 「actType」表明這個按鈕的動做類型(如:編輯、刪除、審覈等),這個屬性是資權限的接口返回的,前端在調這個接口時將這個屬性記錄下來,或者保存到對應的控件中。因此前端能夠不用關於這個屬性的每一個枚舉值表明的是什麼含義,只需根據接口的返回值賦值就好。 用興趣的同窗也能夠參考一下actType取值以下:1 可讀,2 編輯,3 可讀+可寫, 4 可收貨,8 可發貨,16 可配貨, 32 可審覈,64 可完結
對於有權限控制的業務數據,列表接口或者詳情接口都會返回一個「permissionType」的字段,這個字段表明當前用戶對於這條業務數據的權限類型,如當 permissionType=2 表明這個用戶對於這條數據有【編輯權限】,permisionType=4 表明這個用戶對於這條業務數據有收貨的權限,permisionType=6表示這個用戶對於這條記錄用編輯和發貨的權限(6=2+4)
如今列表上有三個按鈕,【編輯】、【收貨】、【完結】,它們對應的「actType」分別爲二、四、64,某一條數據的permissionType=3,這時這三個按鈕的狀態怎麼判斷呢,permissionType=3 咱們能夠分解爲 1+2,表示這個用戶對於這條記錄有「可讀」+「編輯」權限,則這三個按鈕中,只有【編輯】按鈕是可用的。那麼判斷的公式爲:
((data[i].permissionType & obj.actType)==obj.actType)
複製代碼
須要進行數據轉換
接口mock返回數據
response = [{
"type": 3,
"name": "建立活動-10001",
"actType": 0,
"code": "10001"
}, {
"type": 3,
"name": "編輯-10002",
"actType": 2,
"code": "10002"
}, {
"type": 3,
"name": "配置-10005",
"actType": 4,
"code": "10005"
}, {
"type": 3,
"name": "訂閱警報-10006",
"actType": 8,
"code": "10006"
}, {
"type": 3,
"name": "查詢詳情-20001",
"actType": 16,
"code": "20001"
}, {
"type": 3,
"name": "批量操做-10007",
"actType": 32,
"code": "10007"
}, {
"type": 3,
"name": "更多操做-10008",
"actType": 64,
"code": "10008"
}]
複製代碼
每個返回的接口權限會將對應的actType一塊兒返回。
getAuthorityForPage代碼修改 簡單修改一下,由於以前返回的是code數組,如今返回的是對象
/** * 獲取當前頁面的權限控制 */
*getAuthorityForPage({ payload }, { put, call, select }) {
// 這裏的資源code都是本身加載的
const pageCodeArray = yield select(state => state.globalAuthority.pageCodeArray);
const response = yield call(getAuthority, pageCodeArray);
const hasAuthorityCodeArray = response || [];
const codeAuthorityObject = {};
pageCodeArray.forEach((value, index, array) => {
codeAuthorityObject[value] = hasAuthorityCodeArray.map(item => item.code).indexOf(value) >= 0;
});
// debugger
yield put({
type: 'save',
payload: {
hasAuthorityCodeArray,
codeAuthorityObject,
},
});
},
複製代碼
修改AuthorizedButton代碼 增長數據權限判斷
/* eslint-disable eqeqeq */
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.map(item => item.code).indexOf(code) >= 0 && this.checkDataAuthority(); // 資源權限
};
/** * 檢測數據權限 */
checkDataAuthority = () => {
const {
globalAuthority: { hasAuthorityCodeArray },
code, // 當前按鈕的code
actType, // 當前按鈕的actType的值經過傳遞傳入
recordPermissionType, // 單條數據的數據操做權限總和
actTypeArray
} = this.props;
if (recordPermissionType || actTypeArray) { // 單條數據權限校驗
const tempCode = hasAuthorityCodeArray.filter(item => item.code === code)
let tempActType = ''
if (actType) {
tempActType = actType
} else if (tempCode.length) {
tempActType = tempCode[0].actType
} else {
return true; // 默認返回true
}
if (actTypeArray) { // 批量操做
return !actTypeArray.some(item => !this.checkPermissionType(item.toString(2), tempActType.toString(2)))
}
// 單條數據操做
return this.checkPermissionType(recordPermissionType.toString(2), tempActType.toString(2))
}
return true; // 若是字段沒有值的狀況下,證實不須要進行數據權限
}
/** * 二進制檢查當前當前數據是否具備當前權限 * @param {*} permissionType * @param {*} actType */
checkPermissionType = (permissionType, actType) =>
(parseInt(permissionType,2) & parseInt(actType,2)).toString(2) == actType
render() {
const { children, code } = this.props;
return (
<span style={{ display: this.checkAuthority(code) ? 'inline' : 'none' }}>{children}</span>
);
}
}
export default AuthorizedButton;
複製代碼
調用方式
單條數據操做
<AuthoriedButton code="10005" recordPermissionType={record.permissionType}>
<a onClick={() => this.handleUpdateModalVisible(true, record)}>配置</a>
</AuthoriedButton>
複製代碼
批量操做
<AuthoriedButton code="10007" actTypeArray={getNotDuplicateArrayById(selectedRows, 'permissionType')}>
<Button>批量操做</Button>
</AuthoriedButton>
複製代碼