上文 通用數(shù)據(jù)權(quán)限設(shè)計——列權(quán)限(一)說了列權(quán)限的設(shè)計理念和整體架構(gòu),下面來說說具體實現(xiàn)
疑問
下面我們從疑問入手暑诸,從問題出發(fā)來看字段權(quán)限的具體祥設(shè):
- 攔截器或者鉤子函數(shù)應(yīng)該從哪兒入手惨缆,什么時候開始接入序列化返回的進行我們的邏輯處理;
- 怎么獲取配置的字段權(quán)限剂桥,有可能在數(shù)據(jù)庫忠烛,有可能在緩存;
- 字段權(quán)限的配置表如何擴展权逗,比如增加修改時間;
- 返回類型不一致美尸,如何從封裝類獲取斟薇;
- 如何做到動態(tài)配置想處理的字段师坎;
- 若已有策略處理(加密、脫敏堪滨、清除胯陋、混淆)不滿足需求,需要復(fù)寫加密袱箱,或新增策略如何實現(xiàn)遏乔;
- 如何實現(xiàn)動態(tài)序列化
代碼
Talk is cheap. Show me the code.
地址:代碼后期在考慮上傳開源,已封裝為starter发笔,引入依賴盟萨,加上注解即可使用
image.png
處理器核心流程圖
UML
Q&A
- 攔截器或者鉤子函數(shù)應(yīng)該從哪兒入手;
根據(jù)上篇文章內(nèi)容了讨,我們需要在序列化之前對返回內(nèi)容進行攔截處理捻激,spring boot 序列化默認采用是 jackson 實現(xiàn),如圖量蕊;
GenericHttpMessageConverter的實現(xiàn)類.png
我們需要重寫org.springframework.http.converter.json.MappingJackson2HttpMessageConverter & writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
方法铺罢,在序列化之前執(zhí)行對象的字段處理即可,通用返回的code置換msg也可在此處理但此種處理方式存在局限性残炮,當項目使用fastjson\gson等其他序列化組件時韭赘,又需要額外重寫序列化邏輯,違背通用性的原則势就;
查看源碼發(fā)現(xiàn)org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor & writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
方法主要對返回結(jié)果進行處理
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執(zhí)行return之后泉瞻,在response返回給瀏覽器或者APP客戶端之前脉漏,執(zhí)行的對response的一些處理⌒溲溃可以實現(xiàn)對response數(shù)據(jù)的一些統(tǒng)一封裝或者加密等操作
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);
// 對象轉(zhuǎn)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;
}
}
}
所以我們只要實現(xiàn)ResponseBodyAdvice接口侧巨,重寫beforeBodyWrite方法,在方法中進行對象字段處理
- 怎么獲取配置的字段權(quán)限鞭达,有可能在數(shù)據(jù)庫司忱,有可能在緩存;
對于權(quán)限配置表所對應(yīng)的類畴蹭,組件不應(yīng)該提供具體實現(xiàn)坦仍,參考spring security 中UserDetails 的設(shè)計思路,提供接口叨襟,讓使用者去具體實現(xiàn)繁扎,代碼如下:
/**
* @title: FiledAuth
* @projectName born
* @description: 字段權(quán)限數(shù)據(jù)庫配置接口
* @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: 獲取字段配置的緩存接口,用戶需自己實現(xiàn)
* @author summer
* @date 2021/7/26 16:00
*/
public interface FieldPermissionExt {
/**
* @title GetColumnConfigForCache
* @description 返回緩存中配置的字段權(quán)限信息
* @author summer
* @return List<Map<String, FiledAuth>>
* @updateTime 2021/7/26 16:01
*/
List<Map<String, FiledAuth>> getColumnConfigForCache();
這樣組件無需關(guān)注關(guān)注配置信息的字段設(shè)計和配置信息的存儲位置糊闽,通過接口進行約束梳玫,并通過多態(tài)運行時獲取bean,從而獲取字段配置信息右犹;
- 字段權(quán)限的配置表如何擴展提澎,比如增加修改時間;
參考問題2,F(xiàn)iledAuth 為接口傀履,使用者可以在實現(xiàn)類中加上自己需要的額外字段
- 返回類型不一致虱朵,如何從封裝類獲取
/**
* @title: FieldPermissionExt
* @projectName born
* @description: 獲取字段配置的緩存接口莉炉,用戶需自己實現(xiàn)
* @author summer
* @date 2021/7/26 16:00
*/
public interface FieldPermissionExt {
/**
* @title GetColumnConfigForCache
* @description 返回緩存中配置的字段權(quán)限信息
* @author summer
* @return List<Map<String, FiledAuth>>
* @updateTime 2021/7/26 16:01
*/
List<Map<String, FiledAuth>> getColumnConfigForCache();
/**
* @title beforeHandlerReturnObj
* @description 用于擴展返回結(jié)果非單個對象或list包裹對象數(shù)據(jù)
* @author summer
* @return Object 只能返回業(yè)務(wù)數(shù)據(jù)對象或包裹數(shù)據(jù)的List集合钓账,否則拋異常
* @updateTime 2021/7/27 14:15
*/
Object beforeHandlerReturnObj(Object body);
/**
* @title afterHandlerReturnObj
* @description 用于擴展返回結(jié)果非單個對象或list包裹對象數(shù)據(jù)
* @author summer
* @return Object 只能返回業(yè)務(wù)數(shù)據(jù)對象或包裹數(shù)據(jù)的List集合,否則拋異常
* @updateTime 2021/7/27 14:15
*/
Object afterHandlerReturnObj(Object Target, Object Original);
}
beforeHandlerReturnObj 和 afterHandlerReturnObj 分別為前置和后置處理器絮宁,此處借鑒spring中BeanPostProcessor的思想梆暮,在處理具體對象時,如果對象進行深度封裝绍昂,組件無法遍歷獲取想要處理的對象啦粹,則用戶擴展實現(xiàn)該接口,將預(yù)處理對象返回窘游,在核心處理器處理完畢后再裝載回去唠椭;
- 如何做到動態(tài)配置想處理的字段;
由FiledAuth接口中g(shù)etProcessor(處理器)和getProcessColumn(處理字段)可知忍饰,web后臺可以動態(tài)對該字段進行賦值贪嫂,而核心處理器可以根據(jù)策略模式進行調(diào)用對應(yīng)處理方法進行處理,代碼如下:
/**
* @title: ColumnContext
* @projectName born
* @description: 字段處理環(huán)境類
* @author summer
* @date 2021/7/23 9:38
*/
public class ColumnContext {
@Autowired
private Processors processors;
/**
* @title executeStrategy
* @description 字段權(quán)限處理的執(zhí)行類
* @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);
}
- 若已有策略處理(加密艾蓝、脫敏力崇、清除斗塘、混淆)不滿足需求,需要復(fù)寫加密亮靴,或新增策略如何實現(xiàn)馍盟;
組件首先提供一個處理器接口,包含獲取策略實現(xiàn)類茧吊,注冊新的策略贞岭,代碼如下
/**
* @title: Processors
* @projectName born
* @description: 獲取處理類型的頂層接口
* @author summer
* @date 2021/8/3 10:19
*/
public interface Processors {
/**
* @title getStrategyObject
* @description 根據(jù)處理器類型返回對應(yīng)的策略實現(xiàn)類
* @author summer
* @return ColumnStrategy 策略的具體實現(xiàn)類
* @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);
}
其次組件提供了抽象類實現(xiàn)該接口曹步,用于實現(xiàn)Processors接口
/**
* @title: ColumnProcess
* @projectName born
* @description: 獲取處理類型的抽象類
* @author summer
* @date 2021/8/3 9:57
*/
public abstract class ColumnProcessAbstract implements Processors{
// 存放策略實現(xiàn)類的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);
}
}
接著一個默認實現(xiàn)集成該抽象類
/**
* @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);
}
}
最后,若用戶需要復(fù)寫或新增休讳,則直接繼承抽象類即可讲婚,代碼如下:
/**
* @title: MyColumnProcess
* @projectName born
* @description: 測試-模擬修改默認的加密方法,改為自定義實現(xiàn)
* @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接口有多個實現(xiàn)俊柔,spring 是怎么實現(xiàn)掃描加載替換掉默認實現(xiàn)和排序的筹麸,根據(jù)源碼排查對應(yīng)邏輯如下:
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;
}