这是我参与更文挑战的第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>
复制代码