• 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正则表达式复习
    • 查看原文如何配合权限分配要求?
    • ......

    这些往细里说都可以单独成篇,闲来再补哈哈

    本篇个网原文
    博客园也会同步记录!

  • 相关阅读:
    join()方法作用
    多线程的运行状态
    守护线程和非守护线程
    多线程快速入门
    Spring Boot2.0之注解方式启动Springmvc
    Spring Boot2.0之 原理—创建内置Tomcat容器
    Spring Boot2.0之纯手写框架
    Sprin Boot2.0之整合Mybatis整合分页插件
    linux下通过acl配置灵活目录文件权限(可用于ftp,web服务器的用户权限控制)
    PHP编程效率的20个要点
  • 原文地址:https://www.cnblogs.com/noodlerkun/p/15471424.html
Copyright © 2020-2023  润新知