设计细节-----适配粒度权限设计
不积跬步无以至千里,在这里,总结一下自己最近的工作积累。
前言:CRM&OA类系统的设计负责度往往是在业务上,而业务上的复杂度,通常是由不同用户在不同业务场景下的各种可操作性决定的。就是说理清了系统用户在不同场景下的操作权限,业务脉络就很清晰了。
记录下,针对最近做的一套管理系统的权限设计方案,复杂性体现在下面的描述当中:
1.这里说的权限是指数据权限以及数据特权,并非菜单权限。
2.默认数据权限规则:
a.登录用户是普通员工,默认情况下(无数据特权)只能看到自己创建的数据,比如说,业务主任下挂接的经销商。
b.登录用户是管理人员,默认情况下(无数据特权)可以看到自己创建的数据,以及所管理部门和部门下子部门下(一直迭代到末端部门节点)所有员工创建的数据。
3.含数据特权
a.登录用户是普通员工,有数据特权,可以看到自己创建的数据以及数据特权部门下所有数据。
b.登录用户是管理人员,有数据特权,可以看到2-b所述的所有数据以及数据特权部门下所有数据。
4.数据特权的赋值最小单位根据业务要求设计为部门,也就是,为某个用户赋值一组数据特权,可以理解为为该用户赋予查询特 权部门下的所有数据
5.非部门组织机构内部人员权限的限定(组织机构外人员不含数据特权)
a.业务要求,组织机构之外,还有两种角色需要参与到业务处理过程当中,属地化业务员:规定可以操作所属经销商下的所有数据
b.经销商:可以操作该用户所有数据
实现思路:
设计严格遵守开闭原则,在需要授权的地方,过滤权限。代码入口为
--UserUtils.screeningAndProcessingEn(param);
例子部分代码如下:
public JSONObject getPageList(Map param) throws Exception {
param.put("search_type", "1");
UserUtils.screeningAndProcessingEn(param);
.................
BeanUtils.populate(dto, param);
................
try{
getTimeRange(kjTerminalNode);
lists = KjTerminalNodeService.appOauthFindList( kjTerminalNode);
...................
return json;
}
@SuppressWarnings("unchecked")
public static void screeningAndProcessingEn(Map params) throws Exception {
// TODO Auto-generated method stub
List<String> users = new ArrayList<String>();
Map<String, String> role = getRole(params);
if (Tools.str(role.get("roleName"), "").indexOf("属地业务员") >= 0) {
users.add(Tools.str(role.get("id"), ""));
params.put("dealerList", kjDealerCoverMapper.getDealerBySd(users));
} else if (Tools.str(role.get("roleName"), "").indexOf("经销商") >= 0) {
List<String> list = new ArrayList<>();
list.add(Tools.str(role.get("id"), ""));
params.put("dealerList",list);
}else {
UserUtils.setUserAndOffice(params);
/*users.addAll(dto.getUserList());
results.addAll(kjDealerCoverMapper.getDealerByBm(users));*/
//业务主任及以上,不通过经销商获取数据
}
if (Tools.str(params.get("search_type"), "").equals("1")) {
UserUtils.searchProcessing(params);
}else if (Tools.str(params.get("search_type"), "").equals("2")) {
UserUtils.searchProcessing2(params);
}
}
@SuppressWarnings("unchecked")
public static void setUserAndOffice(Map params){
//数据权限
User user = UserUtils.getByLoginName((String)params.get("loginName"));
/*AuthorityResponse<Office> offices = officeService.getAutorOfficeEh(user.getId());
if(null != offices.getResults() && offices.getResults().size() > 0) {
List<String> list = new ArrayList<>();
for (Office item : offices.getResults()) {
list.add(item.getId());
}
params.put("officeList", list);
}*/
//不再使用获取部门,获取部门封装到sql层直接调用
String offices = userMapper.getOfficeString(user);
if (StringUtils.isNotBlank(offices)) {
params.put("officeList", Tools.asList(Tools.str(offices, "").split("[|]")));
LOGGER.debug("==============当前登录者可以查看部门="+JSONArray.fromObject(Tools.asList(offices.split("[|]"))).toString());
}else {
LOGGER.debug("==============当前登录者没有可以查看部门");
params.put("officeList",new ArrayList<>());
}
List<String> list2 = new ArrayList<>();
list2.add(user.getId());
params.put("userList", list2);
LOGGER.debug("当前登录者可以查看科室="+params.get("officeList")+"==============当前登录者可以查看用户="+params.get("userList"));
//searchProcessing(params);
} -->screeningAndProcessingEn的具体实现细节-->setUserAndOffice(params);
-->userMapper.getOfficeString(user);
-->getAutorOfficeEh(user.getId());
-->searchProcessing(params);
权限过滤采用约定好的封装数据格式,然后层层装配数据,生成相应的查询sql,在dao层实现数据的统一过滤,和业务完全解耦,只需要在业务开始前,执行screeningAndProcessingEn()方法即可完成对权限的过滤,或者是在方法上标上@PerminssionFilter注解即可。根据自定义annotation结合aspect的前置通知实现,提供了对权限过滤的注解支持。
setUserAndOffice根据约定好的数据格式,封装好该登录用户可以查看的部门,具体实现是userMapper.getOfficeString(user)和getAutorOfficeEh(user.getId());两种方式。userMapper.getOfficeString(user)的设计是基于mysql函数(写了一个根据用户id,根据一定规则获取部门的函数),设计他是因为最早的方案getAutorOfficeEh(user.getId());是在代码层面根据权限规则递归获取部门等信息,经过测试,发现响应比较慢,一条设计到过滤权限的查询,有时需要3S才能响应。
给出getAutorOfficeEh(user.getId())的代码实现:
public AuthorityResponse<Office> getAutorOfficeEh(String userId,Boolean onlyDownLevel,Boolean size ){
List<Office> results = Lists.newArrayList();
//List<Office> results = new ArrayList<Office>();
AuthorityResponse<Office> response = new AuthorityResponse<Office>();
User user = UserUtils.get(userId);
String roleType="";
//get current user roles
List<Role> roleList = Lists.newArrayList();
roleList = user.getRoleList();
if (roleList.size()>0) {
//handle the role type
roleType = getHignRoleType(roleList);
}
//这里混入数据特权 since 2018-5-2
String officeIds = user.getOfficeIds();
if (officeIds!=null && ! "".equals(officeIds)) {// has privilege
/************************
* step 3 the type of user role is manager
********************/
if (roleType.equals(RularUtils.MANAGE_TYPE)) {
// handle the situation 1 has privilege and the type of user role is manager
//封装数据特权部门
if (onlyDownLevel) {
handleAuthorityOffice(officeIds,results);
}else {
handleAuthorityOffice1(officeIds,results);
}
//封装本身管辖部门
Office office = user.getOffice();
if (onlyDownLevel) {
if (office != null) {
ergodicUni(results, office);
}
}else {
if (office != null) {
ergodicUni1(results, office);
}
}
response.setStatus(0);
response.setMsg("角色为管理人员,并且有数据特权,部门查询成功");
response.setResults(results);
} else {
/**********************************
* step 4 the type of user role is staff
*******/
// handle the situation 2 has privilege and the type of user role is staff
//封装数据特权部门
if (onlyDownLevel) {
handleAuthorityOffice(officeIds,results);
}else {
handleAuthorityOffice1(officeIds,results);
}
response.setStatus(0);
response.setMsg("角色为普通人员,并且有数据特权,部门查询成功");
response.setResults(results);
}
} else {// do not has privilege
/**********************************
* step 5 the type of user role is manager
*******/
if (roleType.equals(RularUtils.MANAGE_TYPE)) {
// handle the situation 3 has't privilege and the type of user role is manager
Office office = user.getOffice();
if (onlyDownLevel) {
if (office != null) {
ergodicUni(results, office);
}
}else {
if (office != null) {
ergodicUni1(results, office);
}
}
response.setStatus(0);
response.setMsg("角色为管理人员,没有数据特权,部门查询成功");
response.setResults(results);
} else {
// handle the situation 4 has't privilege and the type of user role is staff
if (size) {
results.add(user.getOffice());
}
response.setStatus(0);
response.setResults(results);
response.setMsg("当前用户为普通员工且没有数据特权");
}
}
// 这里混入数据特权 end
return response;
}此方法是权限过滤的核心,是文章开始权限详细说明的代码实现。
searchProcessing(params)权限过滤的最后一步,封装高级查询参数。
最终,过滤权限代码在mapper中生效,通过mybatis动态的生成响应的sql去执行。这个过程是需要约定的
......<where>
a.status = '1' and (1 = 2
<if test="officeList != null and officeList.size() > 0 ">
<foreach item="item" index="index" collection="officeList">
or v.parent_ids like CONCAT(#{item}, '%')
</foreach>
</if>
<if test="userList != null and userList.size() > 0 ">
<foreach item="item" index="index" collection="userList" >
or v.businessMaster_id = #{item}
</foreach>
</if><if test="dealerList != null and dealerList.size() > 0 ">
<foreach item="item" index="index" collection="dealerList">
or v.id = #{item}
</foreach>
</if>
)
<if test="id != null and id != '' ">
AND a.id = #{id}
</if>
<if test="searchOfficeParams != null and searchOfficeParams.size() > 0 ">
<foreach item="item" index="index" collection="searchOfficeParams">
and v.parent_ids like CONCAT(#{item}, '%')
</foreach>
</if>
<if test="searchRegionParams != null and searchRegionParams.size() > 0 ">
<foreach item="item" index="index" collection="searchRegionParams">
and kr.idpath like CONCAT(#{item}, '%')
</foreach>
</if>在此做个总结。