框架选择
权限控制是管理系统必不可少的模块,当前成熟的权限控制框架有shiro,spring security等,利用框架开发的好处就是简洁方便快捷。
本文权限思路——注解方式
本文实现权限的方式是没有使用框架,而是通过自定义注解方式实现简单的权限管理控制。
关于表设计
一般权限表的设计都可以通过5张表来实现,用户表,角色表,权限表,用户角色关系表,角色权限关系表,其中用户与角色是多对多关系,角色与权限也是多对多关系,下图是实现思路举例,具体要根据各自业务逻辑灵活设计。
本文思路是通过自定义注解进行权限控制,表设计如下
其中know_bg_user是用户表,know_bg_user_per_relation是用户权限关系表,与用户表是多对多关系,know_bg_permission是系统权限表,与用户权限关系表是多对多关系,know_bg_sys_module是系统模块表,与系统权限表是一对多关系。
这里是不是有疑问呢,为啥没有角色表月用户角色关系表?表设计是根据业务逻辑来设计的,这里是设定了权限只针对个人用户,不针对角色,也就是说权限与个人绑定,不再与角色绑定。(这种需求虽然说与正常的权限控制看起来很变扭,但是我在实际项目中就有这种需求,所以这里就以这种需求记录)
权限控制实现思路
首先从系统模块表开始,比如这张表里有一些数据,来表示模块,也可认为是菜单
现在需要把这些数据处理成树形结构,因为know_bg_permission系统权限表中有一个模块id,它们是一对多关系,处理成树形结构后,根据模块id去设置权限即可。
具体实现过程
项目结构图如何
1:自定义一个权限注解,这个注解用于标注在controller层方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
//黑名单 0:是 1:不是
int blacklist() default 0;
//权限名称
String permissionName() default "";
//排序
int sort();
//模块名称
String moduleName();
//模块主键,是know_bg_sys_module表的主键,用于权限表和
//模块表建立关联关系
long moduleId();
}
例如添加在添加部门的方法上:
@Autowired
private KnowBgDepartmentService knowBgDepartmentService;
@ApiOperation("新增部门")
@ApiImplicitParams({
@ApiImplicitParam(name = "departmentName", value = "部门名称", required = true, dataType = "string", paramType = "query"),
})
@Permission(permissionName = "添加",sort = 1, moduleName = "部门设置" ,moduleId = 90020)
@RequestMapping(value = "insertDept",method = RequestMethod.POST)
public JsonResult<Integer> insertDept(@RequestParam String departmentName){
return new JsonResult<Integer>(knowBgDepartmentService.insertDept(departmentName),JsonResult.SUCCESS_MESSAGE);
}
2:注解定义好后,定义一个项目启动即执行的方法,预热权限到数据库,这样就不用手动插入
2-1:通过Java反射获取注解方法和注解值
注意:这里是项目启动先删除表里面的权限,再批量新增,这样每次启动权限的主键发生变化,也要同时修改用户关权限关系表里的权限主键!!!这一步至关重要,不然重启后所有用户都没有权限!!!
反射信息实体类
/**
* Copyright (C), 2018-2020
* FileName: ReflectionInfo
* Author: yu.gui.ming
* Date: 2020-06-30 11:54
* Description: 反射信息实体
*/
@Data
public class ReflectionInfo extends SerializeEntity {
//servlet路径
private String url;
//类名
private String className;
//方法
private String method;
//类型
private String type;
}
通过反射获取controller类信息的逻辑类
/**
* Copyright (C), 2018-2020
* FileName: ProjectReflectionController
* Author: yu.gui.ming
* Date: 2020-06-30 11:53
* Description: 权限反射控制器
*/
@RestController
public class ProjectReflectionController {
@Autowired
WebApplicationContext applicationContext;
@Autowired
private KnowBgPermissionMapper knowBgPermissionMapper;
@Autowired
private RedisUtils redisUtils;
@Autowired
private KnowBgUserPerRelationMapper knowBgUserPerRelationMapper;
//控制器类信息list
public static List<ReflectionInfo> reflectInfoList = new ArrayList<ReflectionInfo>();
/**
* 初始化controller类信息
* RequestMappingHandlerMapping在controller中使用
* @return
*/
public void setReflectInfoList(){
try{
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
// 获取url与类和方法的对应信息
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> m : map.entrySet()) {
RequestMappingInfo info = m.getKey();
HandlerMethod method = m.getValue();
PatternsRequestCondition p = info.getPatternsCondition();
ReflectionInfo ref = new ReflectionInfo();
for (String url : p.getPatterns()) {
ref.setUrl(url);
}
ref.setClassName(method.getMethod().getDeclaringClass().getName()); // 类名
ref.setMethod(method.getMethod().getName()); // 方法名
RequestMethodsRequestCondition methodsCondition = info.getMethodsCondition();
for (RequestMethod requestMethod : methodsCondition.getMethods()) {
ref.setType( requestMethod.toString());
}
//去除swagger相关方法
if(!ParamUtils.isNullForParam(ref.getType())){
reflectInfoList.add(ref);
}
}
}finally {
insReqIdentifying();
}
}
/**
* 根据请求url获取方法上的注释名称
* @param url 路径标识
* @return
*/
public Object[] getMethodApiOperation(String url){
Class cls = null;
if(null == reflectInfoList || reflectInfoList.isEmpty() || reflectInfoList.size() <= 0)
throw new BusinessException("init reflectInfoList error");
for (ReflectionInfo entity:reflectInfoList) {
if(entity.getUrl().equals(url)){
try {
cls = Class.forName(entity.getClassName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
Method[] ms = cls.getDeclaredMethods();
Object[] array = new Object[5];
for (Method m:ms) {
if(m.getName().equals(entity.getMethod())){
Permission p = m.getAnnotation(Permission.class);
if(ParamUtils.isNullForParam(p)){
return null;
}else{
array[0] = p.blacklist();
array[1] = p.permissionName();
array[2] = p.sort();
array[3] = p.moduleName();
array[4] = p.moduleId();
return array;
}
}
}
} catch (Exception e){
throw new BusinessException("url 有误");
}
}
}
return null;
}
/**
* 预热权限
*/
public void insReqIdentifying(){
if(null == reflectInfoList || reflectInfoList.isEmpty() || reflectInfoList.size() <= 0){
this.setReflectInfoList();
}
reflectInfoList = reflectInfoList.stream().filter(reflectionInfo -> !ParamUtils.isNullForParam(this.getMethodApiOperation(reflectionInfo.getUrl()))).collect(Collectors.toList());
List<KnowBgPermission> list = new ArrayList<>();
for (ReflectionInfo reflectionInfo :reflectInfoList) {
KnowBgPermission p = new KnowBgPermission();
p.setPermissionId(BaseEntity.getUUID()+ RandomUtils.getRandomNum(8));
Object[] array = this.getMethodApiOperation(reflectionInfo.getUrl());
if(!ParamUtils.isNullForArray(array)){
p.setBlackStatus((Integer)array[0]);
p.setPermissionName((String)array[1]);
p.setSequence((Integer)array[2]);
p.setModuleName((String)array[3]);
p.setModuleId((Long)array[4]);
}
p.setPermissionUrl(reflectionInfo.getUrl());
list.add(p);
}
//更新用户关系表
List<KnowBgPermission> sourcePerList = knowBgPermissionMapper.getAllPermission();
List<KnowBgUserPerRelation> relationList = knowBgUserPerRelationMapper.selectAll();
Map<Long,Long> map = new HashMap<>();
sourcePerList.forEach(s->{
list.forEach(n->{
if(s.getPermissionUrl().equals(n.getPermissionUrl())){
map.put(s.getPermissionId(),n.getPermissionId());
}
});
});
relationList.forEach(re->{
map.forEach((key,value)->{
if(re.getPermissionId().longValue() == key){
re.setPermissionId(value);
}
});
});
knowBgUserPerRelationMapper.delete(null);
knowBgUserPerRelationMapper.batchInsUserPerRelation(relationList);
//更新权限表
knowBgPermissionMapper.delete(null);
knowBgPermissionMapper.batchInsertPermission(list);
//把权限存到缓存
redisUtils.setValue(Constant.SYS_PER_IDENTIFYING, JSON.toJSONString(list));
//给默认用户添加所有权限(一般是超级管理员)
addPerForUser(4616465135465464164L);
addPerForUser(416545654164536545L);
}
/**
* 给用户添加权限
* @param userId
*/
private void addPerForUser(Long userId){
List<KnowBgPermission> list = knowBgPermissionMapper.getAllPermission();
list = list.stream().filter(knowBgPermission -> knowBgPermission.getBlackStatus().intValue() == 0).collect(Collectors.toList());
List<KnowBgUserPerRelation> l = new ArrayList<KnowBgUserPerRelation>();
list.forEach(p->{
KnowBgUserPerRelation r = new KnowBgUserPerRelation();
r.setRelationId(BaseEntity.getUUID()+RandomUtils.getRandomNum(7));
r.setUserId(userId);
r.setPermissionId(p.getPermissionId());
l.add(r);
});
knowBgUserPerRelationMapper.delUserPerRelation(userId);
knowBgUserPerRelationMapper.batchInsUserPerRelation(l);
}
}
2-2:项目启动执行插入方法
/**
* Copyright (C), 2018-2019
* FileName: MyApplicationRunner
* Author: yu.gui.ming
* Date: 219-4-14 17:07
* Description:
*/
@Component
public class MyApplicationRunner implements CommandLineRunner {
@Autowired
private ProjectReflectionController projectReflectionController;
@Override
@Transactional
public void run(String... args) throws Exception {
projectReflectionController.setReflectInfoList();
}
}
插入效果图
3:把权限模块(菜单)和权限处理成树形结构返回前端即可
思路:利用mybatis的一对多映射查询,查询所有的权限模块同时根据模块id顺便查询出权限子列表,然后在业务层处理成树形结构即可。切记:递归查询数据库是大忌!!!正确方式应该是一次查询出所有数据在业务层处理成树形结构。
xml 映射查询代码参考:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="net.lersin.backstage.business.systemSetup.modules.dao.KnowBgSysModuleMapper" >
<resultMap id="BaseResultMap" type="net.lersin.backstage.business.systemSetup.modules.entity.KnowBgSysModule" >
<id column="module_id" property="moduleId" jdbcType="BIGINT" />
<result column="module_name" property="moduleName" jdbcType="VARCHAR" />
<result column="parent_module_id" property="parentModuleId" jdbcType="BIGINT" />
<result column="sequence" property="sequence" jdbcType="INTEGER" />
</resultMap>
<sql id="Base_Column_List" >
module_id, module_name, parent_module_id, sequence
</sql>
<!-- 模块权限 -->
<resultMap id="SecondBaseResultMap" type="net.lersin.backstage.business.systemSetup.modules.vo.ModuleVo">
<id column="module_id" property="moduleId" jdbcType="BIGINT" />
<result column="module_name" property="moduleName" jdbcType="VARCHAR" />
<result column="parent_module_id" property="parentModuleId" jdbcType="BIGINT" />
<result column="permission_name" property="permissionName" jdbcType="VARCHAR" />
<collection property="perVoList" column="module_id" select="net.lersin.backstage.business.systemSetup.permission.dao.KnowBgPermissionMapper.getPerVoListByModuleId"></collection>
</resultMap>
<!-- 获取所有模块 -->
<select id="queryAllModules" resultMap="SecondBaseResultMap" >
select
module_id,
module_name,
parent_module_id
from know_bg_sys_module
order by module_id
</select>
</mapper>
递归处理数据结构参考我另一篇博客(表结构也一样的):
https://blog.csdn.net/Ming13416908424/article/details/107171119
最终swagger数据结构效果:
其中childList 是子模块列表,perVoList是当前模块下的权限列表
{
"state": 200,
"message": "请求成功",
"data": [
{
"moduleId": "100",
"moduleName": "网站概况",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "200",
"moduleName": "知识产品",
"parentModuleId": "0",
"childList": [
{
"moduleId": "20040",
"moduleName": "问题咨询",
"parentModuleId": "200",
"childList": [],
"perVoList": []
},
{
"moduleId": "20050",
"moduleName": "职场故事",
"parentModuleId": "200",
"childList": [],
"perVoList": []
},
{
"moduleId": "20070",
"moduleName": "通知公告",
"parentModuleId": "200",
"childList": [],
"perVoList": []
}
]
},
{
"moduleId": "300",
"moduleName": "企业招聘",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "400",
"moduleName": "品台互动",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "500",
"moduleName": "数据分析",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "600",
"moduleName": "营销管理",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "700",
"moduleName": "会员管理",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "800",
"moduleName": "财务数据",
"parentModuleId": "0",
"childList": []
},
{
"moduleId": "900",
"moduleName": "系统设置",
"parentModuleId": "0",
"childList": [
{
"moduleId": "90010",
"moduleName": "员工设置",
"parentModuleId": "900",
"childList": [],
"perVoList": [
{
"permissionId": "478396832741042564",
"moduleId": "90010",
"permissionName": "添加"
},
{
"permissionId": "478396832756723399",
"moduleId": "90010",
"permissionName": "删除"
},
{
"permissionId": "478396832708138370",
"moduleId": "90010",
"permissionName": "编辑"
},
{
"permissionId": "478396832798881150",
"moduleId": "90010",
"permissionName": "启用/禁用"
},
{
"permissionId": "478396832737060932",
"moduleId": "90010",
"permissionName": "查看"
}
]
},
{
"moduleId": "90020",
"moduleName": "部门设置",
"parentModuleId": "900",
"childList": [],
"perVoList": [
{
"permissionId": "478396832742846512",
"moduleId": "90020",
"permissionName": "添加"
},
{
"permissionId": "478396832721049288",
"moduleId": "90020",
"permissionName": "删除"
},
{
"permissionId": "478396832715416994",
"moduleId": "90020",
"permissionName": "编辑"
},
{
"permissionId": "478396832796742266",
"moduleId": "90020",
"permissionName": "查看"
}
]
},
{
"moduleId": "90030",
"moduleName": "角色设置",
"parentModuleId": "900",
"childList": [],
"perVoList": [
{
"permissionId": "478396832749710159",
"moduleId": "90030",
"permissionName": "添加"
},
{
"permissionId": "478396832781272120",
"moduleId": "90030",
"permissionName": "删除"
},
{
"permissionId": "478396832727406820",
"moduleId": "90030",
"permissionName": "编辑"
},
{
"permissionId": "478396832729774600",
"moduleId": "90030",
"permissionName": "查看"
}
]
}
]
}
]
}
用户权限拦截思路
定义一个拦截器,用户访问时判断用户请求的servletPath路径,判断用户的权限是否包含有此访问路径,有则放行,没有则报异常即可。
拦截器主要方法参考:
/**
* 校验用户权限
* @param queUserVo 用户信息对象(包括基本信息,权限等,是校验token的上一个方法传递下来的)
* @param request 请求对象
*/
private void checkPer(QueUserVo queUserVo, HttpServletRequest request){
//获取权限列表
String servletPath = request.getServletPath();
//从缓存里面取权限
List<KnowBgPermission> perList = (List<KnowBgPermission>) JSON.parseArray(redisUtils.get(Constant.SYS_PER_IDENTIFYING),KnowBgPermission.class);
List<KnowBgPermission> list = new ArrayList<>();
//缓存为空时去查db
if(ParamUtils.isNullForCollection(perList))
knowBgPermissionMapper.getAllPermission().stream().forEach(p->{ if(0 == p.getBlackStatus()) list.add(p); });
else
//这一步是过滤掉白名单,因为一般类似省市区下拉选的接口我们不定义为权限
perList.stream().forEach(p->{ if(0 == p.getBlackStatus()) list.add(p); });
//开始校验用户权限
List<Long> perIds = new ArrayList<Long>();
list.forEach(knowBgPermission -> { perIds.add(knowBgPermission.getPermissionId()); });
list.forEach(p->{
if(p.getPermissionUrl().equals(servletPath)){
if(!queUserVo.getPerIdList().contains(p.getPermissionId())){
throw new BusinessException(BusinessException.PERMISSION_CODE,"没有权限");
}
}
});
}