上文 通用数据权限设计——列权限(一)说了列权限的设计理念和整体架构,下面来说说具体实现
疑问
下面我们从疑问入手,从问题出发来看字段权限的具体祥设:
- 拦截器或者钩子函数应该从哪儿入手,什么时候开始接入序列化返回的进行我们的逻辑处理;
- 怎么获取配置的字段权限,有可能在数据库,有可能在缓存;
- 字段权限的配置表如何扩展,比如增加修改时间;
- 返回类型不一致,如何从封装类获取;
- 如何做到动态配置想处理的字段;
- 若已有策略处理(加密、脱敏、清除、混淆)不满足需求,需要复写加密,或新增策略如何实现;
- 如何实现动态序列化
代码
Talk is cheap. Show me the code.
地址:代码后期在考虑上传开源,已封装为starter,引入依赖,加上注解即可使用
处理器核心流程图
UML
Q&A
- 拦截器或者钩子函数应该从哪儿入手;
根据上篇文章内容,我们需要在序列化之前对返回内容进行拦截处理,spring boot 序列化默认采用是 jackson 实现,如图;
我们需要重写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方法,在方法中进行对象字段处理
- 怎么获取配置的字段权限,有可能在数据库,有可能在缓存;
对于权限配置表所对应的类,组件不应该提供具体实现,参考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
这样组件无需关注关注配置信息的字段设计和配置信息的存储位置,通过接口进行约束,并通过多态运行时获取bean,从而获取字段配置信息;
- 字段权限的配置表如何扩展,比如增加修改时间;
参考问题2,FiledAuth 为接口,使用者可以在实现类中加上自己需要的额外字段
- 返回类型不一致,如何从封装类获取
/**
* @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的思想,在处理具体对象时,如果对象进行深度封装,组件无法遍历获取想要处理的对象,则用户扩展实现该接口,将预处理对象返回,在核心处理器处理完毕后再装载回去;
- 如何做到动态配置想处理的字段;
由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);
}
- 若已有策略处理(加密、脱敏、清除、混淆)不满足需求,需要复写加密,或新增策略如何实现;
组件首先提供一个处理器接口,包含获取策略实现类,注册新的策略,代码如下
/**
* @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);
}
}
- 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;
}