字段权限配置

目录

需求

表设计

代码实现

1、配置annotation

2、配置包含敏感数据的接口字段

3、隐藏策略

4、AOP实现拦截

5、其他工具类包下载地址


需求

查询接口时不返回配置的某角色需要隐藏的字段;

表设计

设计思想: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;
    }


}

5、其他工具类包下载地址

https://download.csdn.net/download/weixin_44765388/77673149

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值