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

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

疑问

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

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

代码

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;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357