前言
- 對於金融機構(比如收單)來說,客戶的賬戶信息的管理,是要滿足一定的安全標准的。其中不限於:密碼,卡號,郵箱,證件...
- 最近所在項目接到這個任務,要求:
- 加密入庫,使用解密,脫敏和原文查看
- 業務無侵入姓
- 易用性 靈活性 性能考量
技術環境
<spring.cloud.alibaba.version>2.2.1.RELEASE</spring.cloud.alibaba.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<mybatisplus.version>3.3.1</mybatisplus.version>
整體思路
- 存庫加密和出庫解密,使用
mybatis 攔截器
- 加密字段的查詢,使用
Aspect切面
,對輸入原文加密后到庫里equals
,該字段將放棄模糊查,有更好的方案歡迎交流... - 顯示端應用脫敏有兩種
- 前后端分離的考慮
JSON轉換器
- 不分離的考慮
Aspect切面
,本篇使用切面
- 前后端分離的考慮
logback
日志輸出脫敏的話,暫采用的方案是,對輸出的日志字符串進行正則匹配替換,有更好的辦法歡迎介紹啊
Code1:mybatis-plus攔截器實現入庫加密出庫解密
- 攔截添加和更新操作
/**
* 更新操作數據加密
* @作者 richardhe
* @創建時間 2021年7月22日 下午1:38:35
* @版本 1.0
*/
@Intercepts({
// 增刪改
@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})
})
@Slf4j
public class MybatisUpdateEncryptInterceptor implements Interceptor {
@Autowired
private CryptService cryptService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
/**
* 前置處理 implement pre processing if need
*/
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
log.debug("sqlCommandType:{}", sqlCommandType);
Object param = null;
// 新增操作
if(sqlCommandType == SqlCommandType.INSERT) {
param = invocation.getArgs()[1];
// 加密
cryptService.encryptBean(param);
}
// 更新操作
else if(sqlCommandType == SqlCommandType.UPDATE) {
// 實體參數
ParamMap<?> parameter = (ParamMap<?>) invocation.getArgs()[1];
param = parameter.values().toArray()[0];
// 加密
cryptService.encryptBean(param);
}
// 真正的 Excutor 執行
Object returnObject = invocation.proceed();
return returnObject;
}
}
- 攔截查詢操作
/**
* 查詢結果數據解密
* @作者 richardhe
* @創建時間 2021年7月22日 下午1:38:35
* @版本 1.0
*/
@Intercepts({
@Signature(type= ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class MybatisQueryDecryptInterceptor implements Interceptor {
@Autowired
private CryptService cryptService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 真正的 Excutor 執行
Object returnObject = invocation.proceed();
/**
* 后置處理 implement post processing if need
*/
if(null == returnObject) {
return returnObject;
}
// handleResultSets返回結果一定是一個List
// size為1時,Mybatis會取第一個元素作為接口的返回值。
if(returnObject instanceof List) {
List<?> rsList = (List<?>) returnObject;
// 解密
decrypt(rsList);
}
return returnObject;
}
private void decrypt(List<?> rsList) {
// 目標類
if(CollUtil.isEmpty(rsList) || !rsList.get(0).getClass().isAnnotationPresent(Crypt.class)) {
return;
}
for(Object o:rsList) {
cryptService.decryptBean(o);
}
}
}
Code2-顯示端應用切面掩碼脫敏
/**
* 切面,敏感數據加掩碼<br>
* @作者 richardhe
* @創建時間 2021年10月19日 上午11:35:15
* @版本 1.0
*/
@Slf4j
@Aspect
@Component
public class DataResponseMaskAspect {
@Autowired
private XxxAppProperties appProps;
@PostConstruct
private void init() {
log.info("{} 切面組件ioc注入", this.getClass().getSimpleName());
}
// 連接點1,針對Feign正常查詢
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
private void pointcut1() {}
@Pointcut("execution(* com.xxx.xxx.feign..*.get*(..))")
private void pointcut11() {}
@AfterReturning(value = "pointcut1() && pointcut11()", returning = "result")
public void adviceAfterReturning1(Object result) {
maskResult(result);
}
// 連接點2,針對Feign分頁查詢
@Pointcut("execution(* com.xxx.xxx.feign..*.queryPage(..))")
private void pointcut2() {}
@AfterReturning(value = "pointcut2()", returning = "result")
public void adviceAfterReturning2(Object result) {
maskResult(result);
}
// 連接點3,其他單獨加約定的@Mask注解
@Pointcut("@annotation(com.xxx.crypt.mask.Mask)")
private void pointcut3() {}
@AfterReturning(value = "pointcut3()", returning = "result")
public void adviceAfterReturning3(Object result) {
maskResult(result);
}
/**
* 加掩碼邏輯
* @param result
*/
private void maskResult(Object result) {
// 備用開關
if(appProps.isCloseMaskFunction()) {
return;
}
// 分頁查詢
if(result instanceof PageResp) {
PageResp<?> rs = (PageResp<?>)result;
if(rs.isSuccess() && CollUtil.isNotEmpty(rs.getPage().getRecords())
&& rs.getPage().getRecords().get(0).getClass().isAnnotationPresent(Mask.class)) {
rs.getPage().getRecords().forEach(o -> BaseMasker.maskBean(o));
}
return;
}
// 非分頁查詢
if(result instanceof BaseResp) {
BaseResp<?> rs = (BaseResp<?>)result;
if(rs.isSuccess()){
BaseMasker.maskTObject(rs.getData());
}
}
}
}
Code3-敏感字段查詢功能-切面
- 目前方案,對輸入的進行同等加密,然后去庫里equals,這意味着放棄這些字段的模糊查詢功能,有更好方案歡迎介紹...
/**
* 切面,查詢參數加密<br>
* @作者 richardhe
* @創建時間 2021年10月19日 上午11:35:15
* @版本 1.0
*/
@Slf4j
@Aspect
@Component
@ConditionalOnBean(CryptService.class)
public class QueryParamEncryptAspect {
@Autowired
private AppProperties appProps;
@Autowired
private CryptService cryptService;
@PostConstruct
private void init() {
log.info("{} 切面組件ioc注入", this.getClass().getSimpleName());
}
// 連接點1,分頁查處理
@Pointcut("execution(* com.xxx.**.controller.*Controller.queryPage(..))")
private void pointcut1() {}
@Before(value = "pointcut1()")
public void adviceBefore1(JoinPoint joinPoint) {
if(appProps.isCloseQueryParamEncrypt()) {
return;
}
// 方法參數
Object[] args = joinPoint.getArgs();
if(ArrayUtil.isNotEmpty(args)) {
BaseReq<?,?> pageReq = (BaseReq<?,?>) args[0];
Object cond = pageReq.getCond();
// 對象字段加密
cryptService.encryptBean(cond);
}
}
// 連接點2,目標注解方法,注意!!!參數請不要使用primitive type(int和long和boolean),請使用包裝類
@Pointcut("@annotation(com.xxx.crypt.Crypt)")
private void pointcut2() {}
@Around(value = "pointcut2()")
public Object adviceBefore2(ProceedingJoinPoint joinPoint) throws Throwable {
if(appProps.isCloseQueryParamEncrypt()) {
return joinPoint.proceed();
}
Object[] newArgs = preHandle(joinPoint);
return joinPoint.proceed(newArgs);
}
private Object[] preHandle(ProceedingJoinPoint joinPoint) {
// 參數列表
Object[] args = joinPoint.getArgs();
if(ArrayUtil.isEmpty(args)) {
return args;
}
Object[] newArgs = args;
// 方法簽名
MethodSignature signature= (MethodSignature) joinPoint.getSignature();
// 方法上的注解,切面已鎖定
//Annotation[] annotations = signature.getMethod().getAnnotations();
// 方法參數注解
Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();
for(int i=0; i<parameterAnnotations.length; i++) {
Annotation[] paramAnntArr = parameterAnnotations[i];
if(ArrayUtil.isEmpty(paramAnntArr) || null==newArgs[i]) {
continue;
}
for(Annotation annt:paramAnntArr) {
// 目標注解參數
if(annt instanceof Crypt) {
Object arg = newArgs[i];
// 字符串類型處理
if(arg instanceof String) {
String str = (String) arg;
String encrypt = cryptService.encrypt(str);
newArgs[i] = encrypt;
// Crypt對象處理
} else if(arg.getClass().isAnnotationPresent(Crypt.class)) {
cryptService.encryptBean(arg);
}
// 其他不處理
}
}
}
return newArgs;
}
}
Code4-logback日志脫敏
- 添加
MessageConverter
/**
* logback 脫敏轉換器
* @作者 ricahrdhe
* @創建時間 2021年10月21日 下午3:31:42
* @版本 1.0
*/
public class SensitiveConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event) {
// INFO及以上的才脫敏處理
if(event.getLevel().isGreaterOrEqual(Level.INFO)) {
return BaseMasker.maskReplaceAll(super.convert(event));
}
return super.convert(event);
}
}
- logback配置
<configuration>
<conversionRule conversionWord="msg" converterClass="com.xxx.crypt.mask.SensitiveConverter"></conversionRule>
...
</configuration>
結語
本篇是從大方向分享下可行性方案,實踐過程中會碰到很多細節坑,比如:
加解密算法
如何選擇?使用第三方服務還是自實現?切面
怎么切好一點?Java反射
復習- 脫敏具體怎么脫?
Java正則表達式
復習 - 查看原文如何配合權限分配要求?
- ......
這些往細里說都可以單獨成篇,閑來再補哈哈