目录
需求
查询接口时不返回配置的某角色需要隐藏的字段;
表设计
设计思想:AOP
传统设计:user-->role,role-->authority;
增加字段权限:user-->role,role-->authority,authority-->block,block-->field;
一个用户拥有多个角色,后台配置每个角色所拥有的权限,权限与区块一对一,需要过滤的接口上加上区块code绑定,区块与重点字段一对多;
field表只需要配置某接口可能需要隐藏的字段,某角色需要隐藏该字段时,将保存至role_field表里;
CREATE TABLE `security_field` (
`id` bigint(20) NOT NULL COMMENT 'id',
`block_id` bigint(20) NOT NULL COMMENT '区块id',
`name` varchar(128) DEFAULT NULL COMMENT '字段标题',
`field` varchar(128) DEFAULT NULL COMMENT '字段值',
`rank` int(11) DEFAULT '0' COMMENT '排序值,从大到小排序',
`remark` varchar(128) DEFAULT NULL COMMENT '备注',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`modify_user` bigint(20) NOT NULL COMMENT '更新人',
`modify_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='字段(配置可能需要隐藏的字段)表';
CREATE TABLE `security_block` (
`id` bigint(20) NOT NULL COMMENT 'id',
`authority_id` bigint(20) NOT NULL COMMENT '页面id',
`name` varchar(128) DEFAULT NULL COMMENT '区块名称',
`code` varchar(128) DEFAULT NULL COMMENT '区块编码',
`rank` int(11) DEFAULT '0' COMMENT '排序值,从大到小排序',
`remark` varchar(128) DEFAULT NULL COMMENT '备注',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`modify_user` bigint(20) NOT NULL COMMENT '更新人',
`modify_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='区块表';
CREATE TABLE `security_role_field` (
`id` bigint(20) NOT NULL COMMENT 'id',
`role_id` bigint(20) NOT NULL COMMENT '角色id',
`block_id` bigint(20) NOT NULL COMMENT '区块id',
`field_id` bigint(20) NOT NULL COMMENT '字段id',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`modify_user` bigint(20) NOT NULL COMMENT '更新人',
`modify_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色字段(配置角色需要隐藏的字段,无则角色所有字段可见)表';
代码实现
1、配置annotation
/**
* 数据字段权限过滤注解
* @author cyl
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataFieldScope {
/**
* 区块权限编码(规则编码,用来获取规则的配置数据)
* @return
*/
String code() default "";
/**
* 注意默认处理的接送数据格式为:{ "data": { "result": } } data->result 必须要data跟result节点,否则不处理,若自定义的话,则按自定义处理
* 这个是用来区分我们处理的json对象的最初开始的节点
* @return
*/
String rulePath() default "";
}
2、配置包含敏感数据的接口字段
如员工列表包含敏感字段员工薪资,希望对某些角色不可见;则在接口上加上注解:
@DataFieldScope(code = "TEST_EMPLOYEE_LIST")block表:增加code为TEST_EMPLOYEE_LIST的一行数据;
field表:增加block_id为上数据的字段为当前薪资的数据;
role_field表:对role_id为6的角色隐藏薪资等字段;
@DataFieldScope(code = "TEST_EMPLOYEE_LIST")
@GetMapping("/list")
@ApiOperation("员工页面")
@ApiOperationSupport(order = 1)
@ApiImplicitParam(paramType = Constants.HEADER, dataType = Constants.STRING, name = Constants.AUTHORIZATION, value = "授权token", required = true)
public Result<QueryPage<EmployeePageVO>> findEmployeePage(EmployeeQuery query) {
QueryPage<EmployeePageVO> page = employeeApplication.findEmployeePage(query);
return Result.ok(page);
}
3、隐藏策略
import lombok.Getter;
/**
* 策略枚举 1:移除属性,2:替换值
* @author xialinlin
*/
@Getter
public enum FiledStrategyEnum {
/**
* 1:移除属性,2:替换值
*/
REMOVE_ATTR(1,"移除属性"),
REPLACE_VALUE(2,"替换值");
private final Integer type;
private final String name;
FiledStrategyEnum(Integer type, String name) {
this.type = type;
this.name = name;
}
}
4、AOP实现拦截
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import com.cms.admin.annotation.DataFieldScope;
import com.cms.admin.security.Securitys;
import com.cms.commons.model.security.user.LoginUserVO;
import com.cms.commons.util.antpathfilter.AntPathFilterMixin;
import com.cms.commons.util.antpathfilter.AntPathPropertyFilter;
import com.cms.commons.util.antpathfilter.FiledStrategy;
import com.cms.commons.util.antpathfilter.FiledStrategyEnum;
import com.cms.domain.security.field.SecurityBlock;
import com.cms.repository.employee.EmployeeRepository;
import com.cms.repository.security.field.SecurityBlockRepository;
import com.cms.repository.security.field.SecurityFieldRepository;
import com.cms.repository.security.field.SecurityPositionFieldRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 数据字段过滤处理
* @author cyl
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DataFieldScopeAspect {
private final String FILTER_NAME = "antPathFilter";
private final EmployeeRepository employeeRepository;
private final SecurityBlockRepository blockRepository;
private final SecurityPositionFieldRepository roleFieldRepository;
private final SecurityFieldRepository fieldRepository;
@Pointcut("@annotation(com.cms.admin.annotation.DataFieldScope)")
public void dataFieldLog() {
}
@Around("dataFieldLog()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
return responseFilterHanlder(point,result);
}
private Object responseFilterHanlder(ProceedingJoinPoint joinPoint,Object result) throws IOException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//获得注解
DataFieldScope annotation = method.getAnnotation(DataFieldScope.class);
if (annotation == null) {
return result;
}
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (null == requestAttributes) {
return result;
}
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
if (null == request) {
return result;
}
// 获取当前的用户
LoginUserVO user = Securitys.user(request);
if (user == null) {
return result;
}
//获取跟数据库中对应的编码
String code = annotation.code();
if(StringUtils.isNotBlank(code)){
//找到当前用户拥有的所有角色,然后通过角色获取需要屏蔽的字段
List<Long> positionIds = employeeRepository.findPositionIdByUserId(user.getId());
//取角色集合 优先级:默认是全部显示,如果有一个角色是显示,一个是不显示的话,则优先显示
// if(CollUtil.isEmpty(positionIds) || positionIds.size() == 0){
// throw new IllegalArgumentException("职位暂无权限");
// }
//获取当前所属的区块信息
SecurityBlock block = blockRepository.findByCode(code);
if(block==null){
return result;
}
//基础重点字段列表
List<String> baseFieldList = fieldRepository.findFieldByBlockId(block.getId());
//如果基础字段为空(已经移除的话),则认为不需要控制了
if(CollUtil.isEmpty(baseFieldList)){
return result;
}
//根据所有角色查找字段,并且该字段是所有角色都存在的字段,则需要纳入过滤中
List<List<String>> allList = new ArrayList<>();
for (Long positionId : positionIds) {
List<String> fieldList = roleFieldRepository.findFieldByPositionIdAndBlockId(positionId, block.getId());
if(CollUtil.isNotEmpty(fieldList)){
allList.add(fieldList);
}else{
//如果多个角色中,只要有一个角色没有排除的字段,则认为都是显示的
allList.clear();
break;
}
}
//将基础的字段加入进去,只有排除的字段的时候,才需要找共有的
if(CollUtil.isNotEmpty(allList)){
allList.add(baseFieldList);
}else{
return result;
}
//查找所有角色中的排除字段,并且该字段是在基础表中存在,并且所有角色关联的排除表中也存在,则认为是需要排除的
List<String> excludeFields = getListIntersection(allList);
//如果没有需要过滤的字段则放行
if(CollUtil.isEmpty(excludeFields)){
return result;
}
String rulePath = annotation.rulePath();
if(StringUtils.isBlank(rulePath)){
rulePath = "data.result.";
}
List<String> ruleFields = new ArrayList<>();
//标识默认显示所有字段
ruleFields.add("**");
for (String field:excludeFields ) {
//加入进来的就是需要进行过滤的
ruleFields.add("!"+field);
}
String [] filterFields = ArrayUtil.toArray(ruleFields,String.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(Object.class, AntPathFilterMixin.class);
objectMapper.registerModule(new JavaTimeModule());
FiledStrategy filedStrategy = new FiledStrategy();
filedStrategy.setType(FiledStrategyEnum.REMOVE_ATTR.getType());
// filedStrategy.setType(FiledStrategyEnum.REPLACE_VALUE.getType());
// filedStrategy.setReplaceValue("*****");
AntPathPropertyFilter filter = new AntPathPropertyFilter(rulePath,filedStrategy,filterFields);
FilterProvider filterProvider = new SimpleFilterProvider().addFilter(FILTER_NAME,filter);
objectMapper.setFilterProvider(filterProvider);
String argsJson = objectMapper.writeValueAsString(result);
return objectMapper.readValue(argsJson, signature.getReturnType());
}
return result;
}
/**
* 获取在所有集合出现过的元素
* @param lists 多个集合
* @return List<String>
*/
private List<String> getListIntersection (List<List<String>> lists) {
if (lists.size() == 1) {
return lists.get(0);
}
List list = new ArrayList(lists.get(0));
for (int i = 1; i < lists.size(); i++) {
List<String> temp = lists.get(i);
list.retainAll(temp);
}
return list;
}
}