美文网首页
通用数据权限设计——列权限(二)

通用数据权限设计——列权限(二)

作者: 新生代民工代表 | 来源:发表于2021-07-23 08:47 被阅读0次

    上文 通用数据权限设计——列权限(一)说了列权限的设计理念和整体架构,下面来说说具体实现

    疑问

    下面我们从疑问入手,从问题出发来看字段权限的具体祥设:

    • 拦截器或者钩子函数应该从哪儿入手,什么时候开始接入序列化返回的进行我们的逻辑处理;
    • 怎么获取配置的字段权限,有可能在数据库,有可能在缓存;
    • 字段权限的配置表如何扩展,比如增加修改时间;
    • 返回类型不一致,如何从封装类获取;
    • 如何做到动态配置想处理的字段;
    • 若已有策略处理(加密、脱敏、清除、混淆)不满足需求,需要复写加密,或新增策略如何实现;
    • 如何实现动态序列化

    代码

    Talk is cheap. Show me the code.
    地址:代码后期在考虑上传开源,已封装为starter,引入依赖,加上注解即可使用


    image.png

    处理器核心流程图

    处理器核心流程图.png

    UML

    列数据权限UML.png
    列数据权限UML.jpg

    Q&A

    1. 拦截器或者钩子函数应该从哪儿入手;
    • 根据上篇文章内容,我们需要在序列化之前对返回内容进行拦截处理,spring boot 序列化默认采用是 jackson 实现,如图;

      GenericHttpMessageConverter的实现类.png
      我们需要重写org.springframework.http.converter.json.MappingJackson2HttpMessageConverter & writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)方法,在序列化之前执行对象的字段处理即可,通用返回的code置换msg也可在此处理
    • 但此种处理方式存在局限性,当项目使用fastjson\gson等其他序列化组件时,又需要额外重写序列化逻辑,违背通用性的原则;
      查看源码发现org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor & writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)方法主要对返回结果进行处理

    if (selectedMediaType != null) {
                selectedMediaType = selectedMediaType.removeQualityValue();
                for (HttpMessageConverter<?> converter : this.messageConverters) {
                    GenericHttpMessageConverter genericConverter =
                            (converter instanceof GenericHttpMessageConverter
                                    ? (GenericHttpMessageConverter<?>) converter
                                    : null);
                    if (genericConverter != null
                            ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType,
                                    selectedMediaType)
                            : converter.canWrite(valueType, selectedMediaType)) {
                        // ResponseBodyAdvice接口是在Controller执行return之后,在response返回给浏览器或者APP客户端之前,执行的对response的一些处理。可以实现对response数据的一些统一封装或者加密等操作
                        body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                                (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                                inputMessage, outputMessage);
                        if (body != null) {
                            Object theBody = body;
                            LogFormatUtils.traceDebug(logger, traceOn -> "Writing ["
                                    + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                            addContentDispositionHeader(inputMessage, outputMessage);
                            // 对象转json
                            if (genericConverter != null) {
                                genericConverter.write(body, targetType, selectedMediaType,
                                        outputMessage);
                            } else {
                                ((HttpMessageConverter) converter).write(body, selectedMediaType,
                                        outputMessage);
                            }
                        } else {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Nothing to write: null body");
                            }
                        }
                        return;
                    }
                }
            }
    

    所以我们只要实现ResponseBodyAdvice接口,重写beforeBodyWrite方法,在方法中进行对象字段处理

    1. 怎么获取配置的字段权限,有可能在数据库,有可能在缓存;

    对于权限配置表所对应的类,组件不应该提供具体实现,参考spring security 中UserDetails 的设计思路,提供接口,让使用者去具体实现,代码如下:

    /**
     * @title: FiledAuth
     * @projectName born
     * @description: 字段权限数据库配置接口
     * @author summer
     * @date 2021/7/23 10:06
     */
    public interface FiledAuth extends Serializable {
    
        String getUserId();
    
        String getUrl();
    
        Integer getProcessor();
    
        String getProcessColumn();
    }
    
    /**
     * @title: FieldPermissionExt
     * @projectName born
     * @description: 获取字段配置的缓存接口,用户需自己实现
     * @author summer
     * @date 2021/7/26 16:00
     */
    public interface FieldPermissionExt {
    
        /**
         * @title GetColumnConfigForCache
         * @description 返回缓存中配置的字段权限信息
         * @author summer
         * @return List<Map<String, FiledAuth>>
         * @updateTime 2021/7/26 16:01
         */
        List<Map<String, FiledAuth>> getColumnConfigForCache();
    

    这样组件无需关注关注配置信息的字段设计和配置信息的存储位置,通过接口进行约束,并通过多态运行时获取bean,从而获取字段配置信息;

    1. 字段权限的配置表如何扩展,比如增加修改时间;

    参考问题2,FiledAuth 为接口,使用者可以在实现类中加上自己需要的额外字段

    1. 返回类型不一致,如何从封装类获取
    /**
     * @title: FieldPermissionExt
     * @projectName born
     * @description: 获取字段配置的缓存接口,用户需自己实现
     * @author summer
     * @date 2021/7/26 16:00
     */
    public interface FieldPermissionExt {
    
        /**
         * @title GetColumnConfigForCache
         * @description 返回缓存中配置的字段权限信息
         * @author summer
         * @return List<Map<String, FiledAuth>>
         * @updateTime 2021/7/26 16:01
         */
        List<Map<String, FiledAuth>> getColumnConfigForCache();
    
        /**
         * @title beforeHandlerReturnObj
         * @description 用于扩展返回结果非单个对象或list包裹对象数据
         * @author summer
         * @return Object  只能返回业务数据对象或包裹数据的List集合,否则抛异常
         * @updateTime 2021/7/27 14:15
         */
        Object beforeHandlerReturnObj(Object body);
    
        /**
         * @title afterHandlerReturnObj
         * @description 用于扩展返回结果非单个对象或list包裹对象数据
         * @author summer
         * @return Object  只能返回业务数据对象或包裹数据的List集合,否则抛异常
         * @updateTime 2021/7/27 14:15
         */
        Object afterHandlerReturnObj(Object Target, Object Original);
    }
    

    beforeHandlerReturnObj 和 afterHandlerReturnObj 分别为前置和后置处理器,此处借鉴spring中BeanPostProcessor的思想,在处理具体对象时,如果对象进行深度封装,组件无法遍历获取想要处理的对象,则用户扩展实现该接口,将预处理对象返回,在核心处理器处理完毕后再装载回去;

    1. 如何做到动态配置想处理的字段;

    由FiledAuth接口中getProcessor(处理器)和getProcessColumn(处理字段)可知,web后台可以动态对该字段进行赋值,而核心处理器可以根据策略模式进行调用对应处理方法进行处理,代码如下:

    /**
     * @title: ColumnContext
     * @projectName born
     * @description: 字段处理环境类
     * @author summer
     * @date 2021/7/23 9:38
     */
    public class ColumnContext {
    
        @Autowired
        private Processors processors;
    
        /**
         * @title executeStrategy
         * @description 字段权限处理的执行类
         * @param: processor
         * @param: targetValue
         * @author summer
         * @return java.lang.String
         * @updateTime 2021/7/23 9:40
         */
        public String executeStrategy(Integer processor, String processColumn) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            ColumnStrategy columnStrategy = processors.getStrategyObject(processor);
            return columnStrategy.handler(processColumn);
        }
    
    }
    
    /**
     * @title: ColumnStrategy
     * @projectName born
     * @description: 字段处理的策略类
     * @author summer
     * @date 2021/7/23 9:36
     */
    public interface ColumnStrategy {
    
        String handler(String processColumn);
    }
    
    1. 若已有策略处理(加密、脱敏、清除、混淆)不满足需求,需要复写加密,或新增策略如何实现;

    组件首先提供一个处理器接口,包含获取策略实现类,注册新的策略,代码如下

    /**
     * @title: Processors
     * @projectName born
     * @description: 获取处理类型的顶层接口
     * @author summer
     * @date 2021/8/3 10:19
     */
    public interface Processors {
    
        /**
         * @title getStrategyObject
         * @description 根据处理器类型返回对应的策略实现类
         * @author summer
         * @return ColumnStrategy 策略的具体实现类
         * @updateTime 2021/8/3 10:45
         */
        ColumnStrategy getStrategyObject(Integer processor);
    
        /**
         * @title registerStrategy
         * @description 注册新的策略,若processor相同,则覆盖之前
         * @author summer
         * @return void
         * @updateTime 2021/8/3 15:17
         */
        void registerStrategy(Integer processor, ColumnStrategy columnStrategy);
    }
    

    其次组件提供了抽象类实现该接口,用于实现Processors接口

    
    /**
     * @title: ColumnProcess
     * @projectName born
     * @description: 获取处理类型的抽象类
     * @author summer
     * @date 2021/8/3 9:57
     */
    public abstract class ColumnProcessAbstract implements Processors{
    
        // 存放策略实现类的map
        private static Map<Integer, ColumnStrategy> STRATEGY_MAP = new HashMap<>();
    
        static{
            // 1:加密
            STRATEGY_MAP.put(1, new EncryptionStrategy());
            // 2:脱敏
            STRATEGY_MAP.put(2, new DesensitizationStrategy());
            // 3:清除
            STRATEGY_MAP.put(3, new ClearStrategy());
            // 4.混淆
            STRATEGY_MAP.put(4, new ObfuscationStrategy());
        }
    
        @Override
        public ColumnStrategy getStrategyObject(Integer processor) {
            return STRATEGY_MAP.get(processor);
        }
    
        @Override
        public void registerStrategy(Integer processor, ColumnStrategy columnStrategy) {
            STRATEGY_MAP.put(processor, columnStrategy);
        }
    
    }
    

    接着一个默认实现集成该抽象类

    /**
     * @title: DefaultColumnProcess
     * @projectName born
     * @description: 获取处理类型的默认装载类
     * @author summer
     * @date 2021/8/3 10:08
     */
    @ConditionalOnMissingBean(Processors.class)
    public class DefaultColumnProcess extends ColumnProcessAbstract {
    
        @Override
        public ColumnStrategy getStrategyObject(Integer processor) {
            return super.getStrategyObject(processor);
        }
    
    }
    

    最后,若用户需要复写或新增,则直接继承抽象类即可,代码如下:

    /**
     * @title: MyColumnProcess
     * @projectName born
     * @description: 测试-模拟修改默认的加密方法,改为自定义实现
     * @author summer
     * @date 2021/8/3 10:26
     */
    @Component
    public class MyColumnProcess extends ColumnProcessAbstract {
    
        public ColumnStrategy getStrategyObject(Integer processor) {
            super.registerStrategy(1,new MyAes());
            return super.getStrategyObject(processor);
        }
    }
    
    1. ResponseBodyAdvice接口有多个实现,spring 是怎么实现扫描加载替换掉默认实现和排序的,根据源码排查对应逻辑如下:
      org.springframework.web.method.ControllerAdviceBean&findAnnotatedBeans(ApplicationContext context)
    /**
         * Find beans annotated with {@link ControllerAdvice @ControllerAdvice} in the
         * given {@link ApplicationContext} and wrap them as {@code ControllerAdviceBean}
         * instances.
         * <p>As of Spring Framework 5.2, the {@code ControllerAdviceBean} instances
         * in the returned list are sorted using {@link OrderComparator#sort(List)}.
         * @see #getOrder()
         * @see OrderComparator
         * @see Ordered
         */
    public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
            List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
            for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
                if (!ScopedProxyUtils.isScopedTarget(name)) {
                    ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
                    if (controllerAdvice != null) {
                        // Use the @ControllerAdvice annotation found by findAnnotationOnBean()
                        // in order to avoid a subsequent lookup of the same annotation.
                        adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
                    }
                }
            }
            OrderComparator.sort(adviceBeans);
            return adviceBeans;
        }
    

    相关文章

      网友评论

          本文标题:通用数据权限设计——列权限(二)

          本文链接:https://www.haomeiwen.com/subject/feyumltx.html