springboot代碼級全局敏感信息加解密和脫敏方案


前言

  • 對於金融機構(比如收單)來說,客戶的賬戶信息的管理,是要滿足一定的安全標准的。其中不限於:密碼,卡號,郵箱,證件...
  • 最近所在項目接到這個任務,要求:
    • 加密入庫,使用解密,脫敏和原文查看
    • 業務無侵入姓
    • 易用性 靈活性 性能考量

技術環境

  • <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正則表達式復習
  • 查看原文如何配合權限分配要求?
  • ......

這些往細里說都可以單獨成篇,閑來再補哈哈

本篇個網原文
博客園也會同步記錄!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM