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

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

疑问

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

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

代码

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>) 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>
     * @updateTime 2021/7/26 16:01
     */
    List> 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>
     * @updateTime 2021/7/26 16:01
     */
    List> 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 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.
     * 

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 findAnnotatedBeans(ApplicationContext context) { List 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; }

你可能感兴趣的:(通用数据权限设计——列权限(二))