一、前言 核心隐私数据无论对于企业还是用户来说尤其重要,因此要想办法杜绝各种隐私数据的泄漏。下面陈某带大家从以下三个方面讲解一下隐私数据如何脱敏,也是日常开发中需要注意的:
配置文件数据脱敏
接口返回数据脱敏
日志文件数据脱敏
文章目录如下:
二、配置文件如何脱敏? 经常会遇到这样一种情况:项目的配置文件中总有一些敏感信息,比如数据源的url、用户名、密码….这些信息一旦被暴露那么整个数据库都将会被泄漏,那么如何将这些配置隐藏呢?
以前都是手动将加密之后的配置写入到配置文件中,提取的时候再手动解密,当然这是一种思路,也能解决问题,但是每次都要手动加密、解密不觉得麻烦吗?
今天介绍一种方案,让你在无感知的情况下实现配置文件的加密、解密。利用一款开源插件:jasypt-spring-boot
。项目地址如下:
使用方法很简单,整合Spring Boot 只需要添加一个starter
。
1. 添加依赖 1 2 3 4 5 <dependency > <groupId > com.github.ulisesbocchio</groupId > <artifactId > jasypt-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency >
2. 配置秘钥 在配置文件中添加一个加密的秘钥(任意),如下:
1 2 3 jasypt: encryptor: password: Y6M9fAJQdU7jNp5MW
当然将秘钥直接放在配置文件中也是不安全的,我们可以在项目启动的时候配置秘钥,命令如下:
1 java -jar xxx.jar -Djasypt.encryptor.password=Y6M9fAJQdU7jNp5MW
3. 生成加密后的数据 这一步骤是将配置明文进行加密,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @SpringBootTest @RunWith(SpringRunner.class) public class SpringbootJasyptApplicationTests { @Autowired private StringEncryptor encryptor; @Test public void encrypt () { String url = encryptor.encrypt("jdbc\\:mysql\\://127.0.0.1\\:3306/test?useUnicode\\=true&characterEncoding\\=UTF-8&zeroDateTimeBehavior\\=convertToNull&useSSL\\=false&allowMultiQueries\\=true&serverTimezone=Asia/Shanghai" ); String name = encryptor.encrypt("root" ); String password = encryptor.encrypt("123456" ); System.out.println("database url: " + url); System.out.println("database name: " + name); System.out.println("database password: " + password); Assert.assertTrue(url.length() > 0 ); Assert.assertTrue(name.length() > 0 ); Assert.assertTrue(password.length() > 0 ); } }
上述代码对数据源的url、user、password进行了明文加密,输出的结果如下:
1 2 3 4 5 database url: szkFDG56WcAOzG2utv0m2aoAvNFH5g3DXz0o6joZjT26Y5WNA+1Z+pQFpyhFBokqOp2jsFtB+P9b3gB601rfas3dSfvS8Bgo3MyP1nojJgVp6gCVi+B/XUs0keXPn+pbX/19HrlUN1LeEweHS/LCRZslhWJCsIXTwZo1PlpXRv3Vyhf2OEzzKLm3mIAYj51CrEaN3w5cMiCESlwvKUhpAJVz/uXQJ1spLUAMuXCKKrXM/6dSRnWyTtdFRost5cChEU9uRjw5M+8HU3BLemtcK0vM8iYDjEi5zDbZtwxD3hA= database name: L8I2RqYPptEtQNL4x8VhRVakSUdlsTGzEND/3TOnVTYPWe0ZnWsW0/5JdUsw9ulm database password: EJYCSbBL8Pmf2HubIH7dHhpfDZcLyJCEGMR9jAV3apJtvFtx9TVdhUPsAxjQ2pnJ
4. 将加密后的密文写入配置 jasypt
默认使用ENC()
包裹,此时的数据源配置如下:
1 2 3 4 5 6 7 8 spring: datasource: username: ENC(L8I2RqYPptEtQNL4x8VhRVakSUdlsTGzEND/3TOnVTYPWe0ZnWsW0/5JdUsw9ulm) password: ENC(EJYCSbBL8Pmf2HubIH7dHhpfDZcLyJCEGMR9jAV3apJtvFtx9TVdhUPsAxjQ2pnJ) driver-class-name: com.mysql.jdbc.Driver url: ENC(szkFDG56WcAOzG2utv0m2aoAvNFH5g3DXz0o6joZjT26Y5WNA+1Z+pQFpyhFBokqOp2jsFtB+P9b3gB601rfas3dSfvS8Bgo3MyP1nojJgVp6gCVi+B/XUs0keXPn+pbX/19HrlUN1LeEweHS/LCRZslhWJCsIXTwZo1PlpXRv3Vyhf2OEzzKLm3mIAYj51CrEaN3w5cMiCESlwvKUhpAJVz/uXQJ1spLUAMuXCKKrXM/6dSRnWyTtdFRost5cChEU9uRjw5M+8HU3BLemtcK0vM8iYDjEi5zDbZtwxD3hA=) type: com.alibaba.druid.pool.DruidDataSource
上述配置是使用默认的prefix=ENC(
、suffix=)
,当然我们可以根据自己的要求更改,只需要在配置文件中更改即可,如下:
1 2 3 4 5 6 jasypt: encryptor: property: prefix: 'PASS(' suffix: ')'
那么此时的配置就必须使用PASS()
包裹才会被解密,如下:
1 2 3 4 5 6 7 8 spring: datasource: username: PASS(L8I2RqYPptEtQNL4x8VhRVakSUdlsTGzEND/3TOnVTYPWe0ZnWsW0/5JdUsw9ulm) password: PASS(EJYCSbBL8Pmf2HubIH7dHhpfDZcLyJCEGMR9jAV3apJtvFtx9TVdhUPsAxjQ2pnJ) driver-class-name: com.mysql.jdbc.Driver url: PASS(szkFDG56WcAOzG2utv0m2aoAvNFH5g3DXz0o6joZjT26Y5WNA+1Z+pQFpyhFBokqOp2jsFtB+P9b3gB601rfas3dSfvS8Bgo3MyP1nojJgVp6gCVi+B/XUs0keXPn+pbX/19HrlUN1LeEweHS/LCRZslhWJCsIXTwZo1PlpXRv3Vyhf2OEzzKLm3mIAYj51CrEaN3w5cMiCESlwvKUhpAJVz/uXQJ1spLUAMuXCKKrXM/6dSRnWyTtdFRost5cChEU9uRjw5M+8HU3BLemtcK0vM8iYDjEi5zDbZtwxD3hA=) type: com.alibaba.druid.pool.DruidDataSource
5. 总结 jasypt还有许多高级用法,比如可以自己配置加密算法,具体的操作可以参考Github上的文档。
三、接口返回数据如何脱敏? 通常接口返回值中的一些敏感数据也是要脱敏的,比如身份证号、手机号码、地址…..通常的手段就是用*
隐藏一部分数据,当然也可以根据自己需求定制。
言归正传,如何优雅的实现呢?有两种实现方案,如下:
第一种方案网上很多实现方式,下面演示第二种,整合Jackson。
1. 自定义一个Jackson注解 需要自定义一个脱敏注解,一旦有属性被标注,则进行对应得脱敏,如下:
1 2 3 4 5 6 7 8 9 10 11 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveJsonSerializer.class) public @interface Sensitive { SensitiveStrategy strategy () ; }
2. 定制脱敏策略 针对项目需求,定制不同字段的脱敏规则,比如手机号中间几位用*
替代,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public enum SensitiveStrategy { USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)" , "$1*$2" )), ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})" , "$1****$2" )), PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})" , "$1****$2" )), ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}" , "$1****$2****" )); private final Function<String, String> desensitizer; SensitiveStrategy(Function<String, String> desensitizer) { this .desensitizer = desensitizer; } public Function<String, String> desensitizer () { return desensitizer; } }
以上只是提供了部分,具体根据自己项目要求进行配置。
3. 定制JSON序列化实现 下面将是重要实现,对标注注解@Sensitive
的字段进行脱敏,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class SensitiveJsonSerializer extends JsonSerializer <String > implements ContextualSerializer { private SensitiveStrategy strategy; @Override public void serialize (String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(strategy.desensitizer().apply(value)); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { Sensitive annotation = property.getAnnotation(Sensitive.class); if (Objects.nonNull(annotation)&&Objects.equals(String.class, property.getType().getRawClass())) { this .strategy = annotation.strategy(); return this ; } return prov.findValueSerializer(property.getType(), property); } }
4. 定义Person类,对其数据脱敏 使用注解@Sensitive
注解进行数据脱敏,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Data public class Person { @Sensitive(strategy = SensitiveStrategy.USERNAME) private String realName; @Sensitive(strategy = SensitiveStrategy.ADDRESS) private String address; @Sensitive(strategy = SensitiveStrategy.PHONE) private String phoneNumber; @Sensitive(strategy = SensitiveStrategy.ID_CARD) private String idCard; }
5. 模拟接口测试 以上4个步骤完成了数据脱敏的Jackson注解,下面写个controller进行测试,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class TestController { @GetMapping("/test") public Person test () { Person user = new Person(); user.setRealName("不才陈某" ); user.setPhoneNumber("19796328206" ); user.setAddress("浙江省杭州市温州市...." ); user.setIdCard("4333333333334334333" ); return user; } }
调用接口查看数据有没有正常脱敏,结果如下:
1 2 3 4 5 6 { "realName" : "不*陈某" , "address" : "浙江省****市温州市..****" , "phoneNumber" : "197****8206" , "idCard" : "4333****34333" }
6. 总结 数据脱敏有很多种实现方式,关键是哪种更加适合,哪种更加优雅…..
四、日志文件如何数据脱敏? 上面讲了配置文件、接口返回值的数据脱敏,现在总该轮到日志脱敏了。项目中总避免不了打印日志,肯定会涉及到一些敏感数据被明文打印出来,那么此时就需要过滤掉这些敏感数据(身份证、号码、用户名…..)。
关于Spring Boot 日志方面的问题有不理解的可以看我之前的文章:Spring Boot第三弹,一文带你搞懂日志如何配置? 、Spring Boot第二弹,配置文件怎么造? 。
下面以log4j2 这款日志为例讲解一下日志如何脱敏,其他日志框架大致思路一样。
1. 添加log4j2日志依赖 Spring Boot 默认日志框架是logback,但是我们可以切换到log4j2,依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-logging</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency >
2. 在/resource目录下新建log4j2.xml配置 log4j2的日志配置很简单,只需要在/resource
文件夹下新建一个log4j2.xml
配置文件,内容如下图:
关于每个节点如何配置,含义是什么,在我上面的两篇文章中有详细的介绍。
上图的配置并没有实现数据脱敏,这是普通的配置,使用的是PatternLayout
3. 自定义PatternLayout实现数据脱敏 步骤 2中的配置使用的是PatternLayout
实现日志的格式,那么我们也可以自定义一个PatternLayout来实现日志的过滤脱敏。
PatternLayout的类图继承关系如下:
从上图中可以清楚的看出来,PatternLayout继承了一个抽象类AbstractStringLayout
,因此想要自定义只需要继承这个抽象类即可。
1、创建CustomPatternLayout,继承抽象类AbstractStringLayout
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 @Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) public class CustomPatternLayout extends AbstractStringLayout { public final static Logger logger = LoggerFactory.getLogger(CustomPatternLayout.class); private PatternLayout patternLayout; protected CustomPatternLayout (Charset charset, String pattern) { super (charset); patternLayout = PatternLayout.newBuilder().withPattern(pattern).build(); initRule(); } private static Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>(); private static Map<String, String> KEY_REG_MAP = new HashMap<>(); private void initRule () { try { if (MapUtils.isEmpty(Log4j2Rule.regularMap)) { return ; } Log4j2Rule.regularMap.forEach((a, b) -> { if (StringUtils.isNotBlank(a)) { Map<String, String> collect = Arrays.stream(a.split("," )).collect(Collectors.toMap(c -> c, w -> b, (key1, key2) -> key1)); KEY_REG_MAP.putAll(collect); } Pattern compile = Pattern.compile(b); REG_PATTERN_MAP.put(b, compile); }); } catch (Exception e) { logger.info(">>>>>> 初始化日志脱敏规则失败 ERROR:{}" , e); } } public String hideMarkLog (String logStr) { try { if (StringUtils.isBlank(logStr) || MapUtils.isEmpty(KEY_REG_MAP) || MapUtils.isEmpty(REG_PATTERN_MAP)) { return logStr; } Set<String> charKeys = KEY_REG_MAP.keySet(); for (String key : charKeys) { if (logStr.contains(key)) { String regExp = KEY_REG_MAP.get(key); logStr = matchingAndEncrypt(logStr, regExp, key); } } return logStr; } catch (Exception e) { logger.info(">>>>>>>>> 脱敏处理异常 ERROR:{}" , e); return logStr; } } private static String matchingAndEncrypt (String msg, String regExp, String key) { Pattern pattern = REG_PATTERN_MAP.get(regExp); if (pattern == null ) { logger.info(">>> logger 没有匹配到对应的正则表达式 " ); return msg; } Matcher matcher = pattern.matcher(msg); int length = key.length() + 5 ; boolean contains = Log4j2Rule.USER_NAME_STR.contains(key); String hiddenStr = "" ; while (matcher.find()) { String originStr = matcher.group(); if (contains) { int i = msg.indexOf(originStr); if (i < 0 ) { continue ; } int span = i - length; int startIndex = span >= 0 ? span : 0 ; String substring = msg.substring(startIndex, i); if (StringUtils.isBlank(substring) || !substring.contains(key)) { continue ; } hiddenStr = hideMarkStr(originStr); msg = msg.replace(originStr, hiddenStr); } else { hiddenStr = hideMarkStr(originStr); msg = msg.replace(originStr, hiddenStr); } } return msg; } private static String hideMarkStr (String needHideMark) { if (StringUtils.isBlank(needHideMark)) { return "" ; } int startSize = 0 , endSize = 0 , mark = 0 , length = needHideMark.length(); StringBuffer hideRegBuffer = new StringBuffer("(\\S{" ); StringBuffer replaceSb = new StringBuffer("$1" ); if (length > 4 ) { int i = length / 3 ; startSize = i; endSize = i; } else { startSize = 1 ; endSize = 0 ; } mark = length - startSize - endSize; for (int i = 0 ; i < mark; i++) { replaceSb.append("*" ); } hideRegBuffer.append(startSize).append("})\\S*(\\S{" ).append(endSize).append("})" ); replaceSb.append("$2" ); needHideMark = needHideMark.replaceAll(hideRegBuffer.toString(), replaceSb.toString()); return needHideMark; } @PluginFactory public static Layout createLayout (@PluginAttribute(value = "pattern") final String pattern, @PluginAttribute(value = "charset") final Charset charset) { return new CustomPatternLayout(charset, pattern); } @Override public String toSerializable (LogEvent event) { return hideMarkLog(patternLayout.toSerializable(event)); } }
关于其中的一些细节,比如@Plugin
、@PluginFactory
这两个注解什么意思?log4j2如何实现自定义一个插件,这里不再详细介绍,不是本文重点,有兴趣的可以查看log4j2
的官方文档。
2、自定义自己的脱敏规则
上述代码中的Log4j2Rule
则是脱敏规则静态类,我这里是直接放在了静态类中配置,实际项目中可以设置到配置文件中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Log4j2Rule { public static Map<String, String> regularMap = new HashMap<>(); public static final String USER_NAME_STR = "Name,name,联系人,姓名" ; public static final String USER_IDCARD_STR = "empCard,idCard,身份证,证件号" ; public static final String USER_PHONE_STR = "mobile,Phone,phone,电话,手机" ; private static String IDCARD_REGEXP = "(\\d{17}[0-9Xx]|\\d{14}[0-9Xx])" ; private static String USERNAME_REGEXP = "[\\u4e00-\\u9fa5]{2,4}" ; private static String PHONE_REGEXP = "(?<!\\d)(?:(?:1[3456789]\\d{9})|(?:861[356789]\\d{9}))(?!\\d)" ; static { regularMap.put(USER_NAME_STR, USERNAME_REGEXP); regularMap.put(USER_IDCARD_STR, IDCARD_REGEXP); regularMap.put(USER_PHONE_STR, PHONE_REGEXP); } }
经过上述两个步骤,自定义的PatternLayout
已经完成,下面将是改写log4j2.xml
这个配置文件了。
4. 修改log4j2.xml配置文件 其实这里修改很简单,原配置文件是直接使用PatternLayout
进行日志格式化的,那么只需要将默认的<PatternLayout/>
这个节点替换成<CustomPatternLayout/>
,如下图:
直接全局替换掉即可,至此,这个配置文件就修改完成了。
5. 演示效果 在步骤3 这边自定义了脱敏规则静态类Log4j2Rule
,其中定义了姓名、身份证、号码这三个脱敏规则,如下:
下面就来演示这三个规则能否正确脱敏,直接使用日志打印,代码如下:
1 2 3 4 @Test public void test3 () { log.debug("身份证:{},姓名:{},电话:{}" ,"320829112334566767" ,"不才陈某" ,"19896327106" ); }
控制台打印的日志如下:
1 身份证:320829 ******566767 ,姓名:不***,电话:198 *****106
哦豁,成功了,so easy!!!
6. 总结 日志脱敏的方案很多,陈某也只是介绍一种常用的,有兴趣的可以研究一下。
五、总结 本篇文章从三个维度介绍了隐私数据的脱敏实现方案,码字不易,赶紧点赞收藏吧!!!
源码已经上传GitHub,需要的公众号码猿技术专栏,回复关键词数据脱敏
获取。