1. 业务开发
可以实现数据维度的权限控制,主要分两种场景;
第一种是数据列表的显示,例如:用户只能看到自己创建的数据或用户只能看到分配给自己的数据,一般使用DataScopeUtil
第二种是数据对应的功能权限控制,例如用户在拥有指定数据访问的前提下在进行功能权限控制,例如只能看数据,不能修改数据或删除数据, 一般使用@DataScope
1.1. 数据范围控制
可以控制目标者(用户)拥有的数据范围
1、使用 DataScopeUtil 在查询列表处调用
2、开发配置页面
1.1.1. DataScopeUtil调用
代码片段摘取自 企业版
public ResultData list(@ModelAttribute @ApiIgnore CategoryEntity category, HttpServletResponse response, HttpServletRequest request, @ApiIgnore ModelMap model, BindingResult result) {
BasicUtil.startPage();
//如果存在分页数据,必须在startPage之后调用
DataScopeUtil.start(this.getPeopleBySession().getId(),this.getLevel(),"role",true);
List categoryList = categoryBiz.query(category);
return ResultData.build().success(new EUListBean(categoryList,(int) BasicUtil.endPage(categoryList).getTotal()));
}
[!tip]
注意DataScopeUtil.start(...)
方法调用位置,如果存在分页必须在BasicUtil.startPage()
之后调用
1.1.2. 开发配置页面
[!tip]
datascope
已经提供了基本的保存、更新接口,开发者只需要编辑一个视图页面调用接口就可以完成数据范围权限开发
1.2. 数据功能控制
控制数据的具体功能权限,实现复杂的针对具体数据的权限功能控制。
1、方法上使用 @DataScope (可选:如果不采用后端控制,后端控制业务更严谨)
2、前端页面视图控制 v-ms-datascope 或 通过 js 控制
3、开发配置页面
[!tip]
与 shiro 不同点:shiro 是系统全局功能权限 ,这里实现的是针对具体数据的功能权限。
1.2.1. @DataScope使用
/**
* 保存文章
* @param content 文章实体
*/
@PostMapping("/save")
@ResponseBody
@ESSave
@LogAnn(title = "保存文章", businessType = BusinessTypeEnum.INSERT)
@RequiresPermissions("cms:content:save")
@DataScope(type="managerRole",id="getCategoryId",requiresPermissions = "cms:content:save")
public ResultData save(@ModelAttribute @ApiIgnore ContentEntity content, HttpServletResponse response, HttpServletRequest request) {
//.....
}
@DataScope(type="业务分类",id="当前方法绑定实体的id主健值,默认getId",requiresPermissions = "对应菜单权限标识")
[!tip]
与 shiro 不同点:shiro 是系统全局功能权限 ,这里实现的是针对具体数据的功能权限。
@DataScope的方法参数必须存在BaseEntity对象
id="getCategoryId" 会调用content实体关联的栏目id
上面配置页面编写好后,还需要在对应的控制调用DataScopeUtil.start方法,用于过滤数据
1.2.2. 前端页面视图控制
引入js
<script src="${base}/static/datascope/index.js"></script>
js业务调用
1、定义 datascopes
变量;
2、定义两个方法 hasPermission
queryDatascope
3、在 created
调用 queryDatascope
var app = new Vue({
el: '#app',
data: {
datascopes: false, //默认所有都没有权限
},
methods: {
//判断是否有权限
hasPermission:function (permission) {
var has = this.datascopes==true || this.datascopes.indexOf(permission)>-1;
return has;
},
//查询权限配置
queryDatascope:function() {
var that = this;
ms.datascope({
dataType: "managerRole",
dataId: this.form.categoryId,
isSuper: true,
}).then((datascopes)=>{
that.datascopes = datascopes;
});
}
},
created: function () {
this.queryDatascope(); //加载权限
}
})
页面调用
v-if="hasPermission('cms:content:save')"
[!tip]
页面中有大量验证的时候推荐使用js业务验证方式,减少接口请求
指令调用
不具备对应功能权限时候,会直接移掉dom元素,类似v-if的效果
v-ms-datascope="{
dataType: 'managerRole',
dataId: form.categoryId,
isSuper: true,
hasModels: 'cms:content:save,cms:content:update'
}"
[!tip]
每调用一次指令都会发起一次验证请求,适合页面中少部分验证,如果验证比较多推荐js业务方式验证;
使用指令 v-ms-datascope 绑定的变量必须在 mounted 进行初始化,否则会出现 指令内部无法绑定上值;
1.2.3. 开发配置页面
事先要根据实际业务情况制作一个权限配置的页面,
例如:角色绑定栏目,可以达到控制角色可以访问哪些栏目下的文章数据,并且可以控制对应栏目的功能权限
[!tip]
datascope
已经提供了基本的保存、更新接口,开发者只需要编辑一个视图页面调用接口就可以完成数据范围权限开发
1.3. 配置页面模版
代码摘取自 企业版
角色绑定栏目
<!DOCTYPE html>
<html>
<head>
<title>管理员权限分配</title>
<#include "../../include/head-file.ftl">
</head>
<body>
<div id="index" v-cloak class="ms-index">
<el-header class="ms-header ms-tr" height="50px">
<@shiro.hasPermission name="datascope:data:save">
<el-button type="primary" icon="iconfont icon-baocun" size="mini" @click="save()" :loading="saveLoading">保存
</el-button>
</@shiro.hasPermission>
</el-header>
<el-main class="ms-container">
<el-alert
class="ms-alert-tip"
title="功能介绍"
type="info"
description="此功能必须超级管理员才能使用,通过对角色的栏目授权,可以控制对应角色下管理人员在对应栏目下的文章管理权限,注意前提是该角色具备文章管理权限,如果不具备这里的设置无效,因为角色的功能权限控制大于这里的权限绑定">
</el-alert>
<div style="display:flex;flex-direction: row;flex:1" v-loading="dataIdLoading">
<div style=" display: flex;position: relative;margin-right: 10px;">
<el-scrollbar>
<el-aside width="400px" style="background-color: #f2f6fc;">
<el-table v-loading="loading" ref="targetIdMultipleTable" height="calc(100vh - 150px)"
class="ms-table-pagination"
border
stripe
:header-cell-class-name="cellClass"
@select="targetIdSelectionRow"
@selection-change="targetIdHandleSelectionChange"
:data="targetIdList" tooltip-effect="dark">
<template slot="empty">
{{emptyText}}
</template>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="需授权的角色名称" align="left" prop="roleName" show-overflow-tooltip>
</el-table-column>
</el-table>
</el-aside>
</el-scrollbar>
</div>
<div style="display: contents;flex-direction: column;flex: 1;margin-left: 220px;">
<el-scrollbar style="width: 100%; overflow-x: hidden">
<el-table ref="multipleTable" :indent="6"
border :data="dataIdList"
stripe
height="calc(100vh - 150px)"
:row-key="(row)=>{return row.id}"
default-expand-all='true'
:tree-props="{children: 'children'}"
:select-on-indeterminate="true"
tooltip-effect="dark"
@select="rowSelect"
@select-all="selectAll"
>
<template slot="empty">
{{dataIdEmptyText}}
</template>
<el-table-column type="selection" width="40" reserve-selection="true"
class-name="isCheck"></el-table-column>
<el-table-column label="栏目名称" align="left" prop="categoryTitle">
</el-table-column>
<el-table-column label="栏目下文章管理权限" align="left">
<template slot-scope="scope" v-if="scope.row.leaf">
<el-checkbox v-for="(model,index) in scope.row.dataIdModelList"
:label="model.modelTitle"
v-model="model.check"
:disabled="!scope.row.check"
:key="scope.row.categoryId+index"></el-checkbox>
</template>
</el-table-column>
</el-table>
</el-scrollbar>
</div>
</div>
</el-main>
</div>
</body>
</html>
<script>
var indexVue = new Vue({
el: '#index',
data: {
saveLoading: false,
manager: ms.manager,
loading: true,
emptyText: '',
targetIdList: [],
checked: 1,
// 右侧数据
dataIdList: [],
//加载状态
dataIdLoading: true,
//提示文字
dataIdEmptyText: '',
//右侧列表选中
dataIdSelectionList: [],
// 右侧树形表格全选
dataIdAllSelect: false,
dataScopeForm: {
dataTargetId: '',
// 数据权限类型
dataType: 'managerRole',
},
},
methods: {
//取消角色列表全选
cellClass(row) {
if (row.columnIndex === 0) {
return 'disabledCheck'
}
},
targetIdQuery: function () {
var that = this;
that.loading = true;
ms.http.get(ms.manager + "/basic/role/list.do", {pageSize: 100}).then(function (data) {
if (data.result) {
that.loading = false;
that.targetIdList = data.data.rows;
}
}).catch(function (err) {
console.log(err);
});
},
targetIdHandleSelectionChange: function (val) {
if (val.length == 0) {
this.dataIdSelectionList = [];
this.dataScopeForm.dataTargetId = "";
this.$refs.multipleTable.clearSelection();
} else if (val.length > 1) {
this.dataIdSelectionList = [];
this.$refs.targetIdMultipleTable.clearSelection()
this.$refs.targetIdMultipleTable.toggleRowSelection(val.pop())
} else {
this.dataScopeForm.dataTargetId = val[val.length - 1].id;
}
},
targetIdSelectionRow: function (selection, row) {
var selected = selection.length && selection.indexOf(row) !== -1; //为true时选中,为 0 时(false)未选中
if (selected) {
this.dataScopeForm.dataTargetId = row.id
this.getDataScopeData();
} else {
this.dataScopeForm.dataTargetId = "";
}
},
//获取选中权限数据
getDataScopeData() {
var that = this;
ms.http.get(ms.manager + "/cms/co/category/categoryList.do", this.dataScopeForm).then(function (data) {
if (data.result) {
that.dataIdLoading = false;
if (data.data.length > 0) {
that.dataIdEmptyText = '';
// that.dataIdList = data.data;
that.dataIdList = ms.util.treeData(data.data, 'id', 'categoryId', 'children');
that.$nextTick(() => {
that.toggleRowSelection(that.dataIdList);
});
} else {
that.dataIdEmptyText = '暂无数据';
that.dataIdList = [];
}
}
}).catch(function (err) {
that.dataIdLoading = false;
console.log(err);
});
},
toggleRowSelection: function (datas) {
datas.forEach(item => {
if (item.check) {
this.$refs.multipleTable.toggleRowSelection(item, true)
}
if (item.children) {
this.toggleRowSelection(item.children);
}
});
},
//权限分配
save: function () {
var that = this;
that.saveLoading = true;
this.dataIdSelectionList = this.$refs.multipleTable.selection;
var _data = new Array();
this.dataIdSelectionList.forEach(item => {
if (item.leaf) {
item.dataTargetId = that.dataScopeForm.dataTargetId;
item.dataType = that.dataScopeForm.dataType;
_data.push(item);
}
})
//如果length==0,代表清空权限
if (_data.length == 0) {
_data.push({dataType: that.dataScopeForm.dataType, dataTargetId: that.dataScopeForm.dataTargetId})
}
ms.http.post(ms.manager + "/cms/co/category/saveBatch.do", _data, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (res) {
that.dataIdLoading = false;
if (res.result) {
that.$notify({
title: '成功',
message: '权限设置成功',
type: 'success'
});
} else {
that.$notify({
title: '失败',
message: res.msg,
type: 'warning'
});
}
that.saveLoading = false;
}).catch(function (err) {
that.dataIdLoading = false;
that.saveLoading = false;
});
},
/*注意在获取初始数据时,所有节点(包括子节点)都增加一个isChecked 标志参数*/
rowSelect(selection, row) {
if (this.dataScopeForm.dataTargetId == "") {
this.$notify({
title: '提示',
message: '请选择需授权的接受名称',
type: 'warning'
});
this.$refs.multipleTable.clearSelection()
return;
}
let selected = selection.length && selection.indexOf(row) !== -1
if (row.children) { //只对有子节点的行响应
if (selected) { //由行数据中的元素isChecked判断当前是否被选中
row.children.map((item) => { //遍历所有子节点
this.$refs.multipleTable.toggleRowSelection(item, true); //切换该子节点选中状态
/*
方法名 说明 参数
用于多选表格,切换某一行的选中状态, row, selected
toggleRowSelection 如果使用了第二个参数,则是设置这一行
选中与否(selected 为 true 则选中)
*/
item.check = true;
item.dataIdModelList.map(model => {
model.check = true;
})
});
row.check = true; //当前行isChecked标志元素切换为false
} else {
row.children.map((item) => {
this.$refs.multipleTable.toggleRowSelection(item, false);
item.check = false;
item.dataIdModelList.map(model => {
model.check = false;
})
});
row.check = false;
}
// console.log(this.multipleSelection, row);
}
//功能按钮选中
if (!row.check) { //如果没有选中
row.dataIdModelList.map(item => {
item.check = true;
})
row.check = true;
} else {
row.dataIdModelList.map(item => {
item.check = false;
})
row.check = false;
}
},
selectAll(selection) {
if (this.dataScopeForm.dataTargetId == "") {
this.$notify({
title: '提示',
message: '请选择需授权的接受名称',
type: 'warning'
});
this.$refs.multipleTable.clearSelection()
return;
}
var that = this;
let dom = document.querySelector(".isCheck>div>label");
if (!dom.className.includes("is-checked")) {
// 全选
that.setSelectAll(that.$refs.multipleTable.data,true)
} else {
// 取消全选
that.setSelectAll(that.$refs.multipleTable.data,false)
}
},
// 设置全选状态的递归函数,arr为递归数组,bool为操作状态(布尔型)
setSelectAll(arr,bool) {
var that = this
arr.forEach(function(item,index) {
item.check = bool
that.$refs.multipleTable.toggleRowSelection(item, bool); //行变选中状态
if(item.children && item.children.length) {
that.setSelectAll(item.children,bool)
}else {
item.dataIdModelList.map(model => {
model.check = bool;
})
}
})
}
},
created: function () {
this.targetIdQuery();
this.getDataScopeData();
}
});
</script>
<style>
/* 去掉全选按钮 */
.el-table .disabledCheck .cell .el-checkbox__inner {
display: none !important;
}
.el-table .disabledCheck .cell::before {
content: '选择';
text-align: center;
line-height: 37px;
}
.el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
}
</style>
[!tip]开发者根据提供的代码模版灵活修改成任意业务场景,主要的依据是 ·权限· 的多对多思路;
接口参考 datascope/data/saveBatch.do ,具体看 swagger-ui.html 描述