业务场景
在前后端分离的情况下,设置某个用户是否有某个列表的编辑、删除等功能权限;设置之后,需要将当前人员是否具有该数据的操作权限,返回给前端,由前端控制相关功能按钮展示;
在结合了不同单位的展示数据,以及是否只控制当前单位数据的操作权限之后,需要在代码中,加入比较多的循环和判断,然后再给每条数据设置权限结果值,比较繁琐;目前通过mybatis拦截器实现了一套符合当前业务的注解,不够完善,但是能够比较方便的使用。
实现目标
在查询人员列表数据的时候,通过注解,直接在每一条返回数据的结构中,设置好是否能够编辑canEdit和是否能够删除canDel的权限值,布尔类型(true:有权限 false:无权限)或者是整形(1:有权限 0:无权限),前端直接通过每条数据中的相关权限字段值,控制是否显示操作按钮
功能实现
方案
第一步:在查询列表的SQL返回结果实体类中,添加相关权限控制字段canEdit,canDel和权限注解
第二步:通过mybatis拦截器,拦截到查询列表的结果集,在结果集中,根据权限注解的配置,以及查询到的结果数据,判断是否给对应返回结果数据的canEdit,canDel字段写入权限值
依赖
<!--数据库 mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
注解
/**
* 操作权限注解
*
* @author likm
* @date 2021/08/06
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Operation {
// authKey,权限值
String value() default "";
//权限返回值是否是布尔,默认false
boolean isBoolean() default false;
//是否限制控制权限为本单位
boolean operationCom() default false;
//设置comId字段名
String comField() default "comId";
}
获取权限值相关
/**
* 用户权限查询返回结构
*
* @author likm
*/
@Data
public class UserAuth implements Serializable {
/**
* 是否所有数据
*/
private Boolean all;
/**
* 是否无权限
*/
private Boolean none;
/**
* 限制的权限值
*/
private List<Integer> ids;
}
枚举
/**
* @author likm
* @date 2021/7/29
* @description 数据范围权限值相关枚举,模板
*/
public enum DataScopeViewTypeEnum {
// 不能查看= 0
//无权限
VIEW_NONE(0, "无权限"),
// 查看所有的= 1
VIEW_ALL(1, "查看所有的"),
// 查看自己的= 2
VIEW_ME(2, "查看自己的"),
// 查看本单位的= 3
VIEW_COMPANY(3, "查看本单位的"),
// 查看本单位及其下级单位的= 4
VIEW_COMPANY_AND_SUB(4, "查看本单位及其下级单位的"),
;
DataScopeViewTypeEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
private Integer code;
private String name;
public Integer getCode() {
return code;
}
public String getName() {
return name;
}
public static DataScopeViewTypeEnum getByValue(Integer value) {
for (DataScopeViewTypeEnum transactType : values()) {
if (transactType.getCode().equals(value)) {
return transactType;
}
}
return null;
}
}
接口
/**
* 数据范围权限需要业务层实现的接口
*
* @Author likm
* @Date 2021/6/24
* @Description //TODO
* @Version 1.0
**/
public interface UserAuthInterface {
/**
* 查询用户对应权限的限制范围值
*
* @param authKey 权限值key
* @return 用户的权限值
*/
UserAuth getUserAuth(String authKey);
}
接口实现
/**
* @Author likm
* @Date 2021/7/29
* @Description //TODO
* @Version 1.0
**/
@Service
public class UserAuthInterfaceImpl implements UserAuthInterface {
@Autowired
@Lazy //不知道咋回事,在拦截器中自动注入mapper之后,会出现循环依赖,暂时未找到好的解决方式,只能通过懒加载注解来解决
private BasicCompanyMapper companyMapper;
@Override
public UserAuth getUserAuth(String authKey) {
UserAuth userAuth = new UserAuth();
//通过authKey和当前用户userId,获取到authKey对应的权限值data
//StorageUtils.getCurrentUserId() 的实现逻辑,是请求参数中在header中带上token,然后使用拦截器进行解析之后,将用户信息存放在threadLocal中
Integer data = basicUserMapper.getAuthDataByKey(StorageUtils.getCurrentUserId(), authKey);
if (!Objects.isNull(authData)) {
switch (DataScopeViewTypeEnum.getByValue(data)) {
case VIEW_NONE:
userAuth.setNone(Boolean.TRUE);
break;
case VIEW_ALL:
userAuth.setAll(Boolean.TRUE);
break;
case VIEW_COMPANY_AND_SUB:
List<BasicCompany> companyList = companyMapper.selectList(new LambdaQueryWrapper<BasicCompany>()
.eq(BasicCompany::getPid, StorageUtils.getCurrentUserComId()));
List<Integer> comIds = companyList.stream().map(BasicCompany::getId).collect(Collectors.toList());
comIds.add(StorageUtils.getCurrentUserComId());
userAuth.setIds(comIds);
break;
case VIEW_COMPANY:
default:
//StorageUtils.getCurrentUserComId() 获取原理同StorageUtils.getCurrentUserId(),都是存放在threadLocal中的数据
userAuth.setIds(Arrays.asList(StorageUtils.getCurrentUserComId()));
break;
}
}
return userAuth;
}
}
mybatis 拦截器实现
/**
* @Author likm
* @Date 2021/9/9
* @Description //TODO
* @Version 1.0
**/
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class OperationInterceptor implements Interceptor {
@Autowired
private UserAuthInterface authInterface;
@Override
public Object intercept(Invocation invocation) throws Throwable {
List resList = new ArrayList();
Object target = invocation.getTarget();
if (target instanceof DefaultResultSetHandler) {
DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
MetaObject metaStatementHandler = SystemMetaObject.forObject(defaultResultSetHandler);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("mappedStatement");
//获取节点属性的集合
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
Class<?> resultType = resultMaps.get(0).getType();
//获取mybatis返回的实体类类型名
int resultMapCount = resultMaps.size();
if (resultMapCount > 0) {
Statement statement = (Statement) invocation.getArgs()[0];
ResultSet resultSet = statement.getResultSet();
//获取表头注解,如果没有,则直接返回
if (!resultType.isAnnotationPresent(Operation.class)) {
//注解为null,原数据返回
return invocation.proceed();
}
if (resultSet != null) {
//获得对应列名
ResultSetMetaData rsmd = resultSet.getMetaData();
Map<String, String> columnMap = new HashMap<>();
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
//用于比较的字段名,变成驼峰
String column = rsmd.getColumnLabel(i);
columnMap.put(HumpLineUtil.lineToHump(column), column);
}
Set<String> columnList = columnMap.keySet();
//根据实体类名称,获取authKey,查询data值,然后字段对应的值
Map<String, Object> operations = getOperations(resultType);
//字段数组
Field[] fields = resultType.getDeclaredFields();
while (resultSet.next()) {
LinkedHashMap<String, Object> resultMap = new LinkedHashMap<>();
for (Field field : fields) {
String colName = field.getName();
Operation operation = field.getAnnotation(Operation.class);
if (columnList.contains(colName)) {
resultMap.put(colName, resultSet.getString(columnMap.get(colName)));
}
if (operations.containsKey(colName)) {
//将需要注解了的字段值,设置为权限值
if (operation.operationCom() && columnList.contains(operation.comField())) {
Integer comId = resultSet.getInt(columnMap.get(operation.comField()));
if (Objects.equals(comId, StorageUtils.getCurrentUserComId())) {
//判断是否限制为当前单位数据
resultMap.put(colName, operations.get(colName));
} else {
resultMap.put(colName, operation.isBoolean() ? Boolean.FALSE : 0);
}
} else {
resultMap.put(colName, operations.get(colName));
}
}
}
Object o = resultType.newInstance();
ConvertUtilsBean convertUtils = BeanUtilsBean
.getInstance()
.getConvertUtils();
//设置,预防bigDecimal类型数据为null导致报错
convertUtils.register(false, false, 0);
//设置,预防Integer类型数据为null时自动转换为0;
convertUtils.register(new IntegerConverter(null), Integer.class);
//设置,预防BigDecimal类型数据为null时自动转换为0;
convertUtils.register(new BigDecimalConverter(null), BigDecimal.class);
BeanUtils.populate(o, resultMap);
resList.add(o);
}
return resList;
}
}
}
return invocation.proceed();
}
private Map<String, Object> getOperations(Class<?> resultType) {
Map<String, Object> operations = new HashMap<>();
Field[] fields = resultType.getDeclaredFields();
Map<String, UserAuth> authData = new HashMap<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Operation.class)) {
Operation operation = field.getAnnotation(Operation.class);
String authKey = operation.value();
if (Objects.equals(authKey, "")) {
continue;
}
UserAuth auth;
//先从map中获取值,避免当前
if (authData.containsKey(authKey)) {
auth = authData.get(authKey);
} else {
//根据authKey,查询对应data值
auth = authInterface.getUserAuth(authKey);
authData.put(authKey, auth);
}
Object value = null;
if (!Objects.isNull(auth.getAll()) && auth.getAll()) {
if (operation.isBoolean()) {
value = Boolean.TRUE;
} else {
value = DataScopeViewTypeEnum.VIEW_ALL.getCode();
}
}
if (!Objects.isNull(auth.getNone()) && auth.getNone()) {
if (operation.isBoolean()) {
value = Boolean.FALSE;
} else {
value = DataScopeViewTypeEnum.VIEW_NONE.getCode();
}
}
if (!Objects.isNull(value)) {
operations.put(field.getName(), value);
} else {
operations.put(field.getName(), operation.isBoolean() ? false : 0);
}
}
}
return operations;
}
/**
* //主要是为了把这个拦截器生成一个代理放到拦截器链中
* ^Description包装目标对象 为目标对象创建代理对象
*
* @Param target为要拦截的对象
* @Return代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注入拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 操作权限拦截器
*/
@Bean
public OperationInterceptor operationInterceptor() {
return new OperationInterceptor();
}
}
使用方式
数据范围权限的使用方式,是在mapper中使用@DataScope注解,在注解中,设置当前sql,所相关权限的authKey值,以及数据范围权限,所限制的表名
@Data
@Operation//这里的注解,控制是否解析当前类,
public class PersonnelVO implements Serializable {
/**
* 主键,自增
*/
private Integer id;
/**
* 用户id
*/
private Integer uid;
/**
* Operation注解,value值是控制该权限的authKey,
* operationCom 表示是否控制为当前单位,
* comField 是返回单位id的字段名
* 返回值,根据传入的isBoolean值,返回Integer或者是Boolean
*/
@Operation(value = "user_list_manage_del", operationCom = true)
private Integer canDel;
@Operation(value = "user_list_manage_edit", isBoolean = true)
private Boolean canEdit;
}
查询
和一般的查询一样,不用做任何修改
@Repository
public interface BasicUserMapper extends BaseMapper<BasicUser> {
List<BasicUser> getList(Page page, Params param);
}
实现效果
不同的列,操作按钮不一样。
其他的rest接口等相关逻辑,就不一一写出来了