项目介绍
日志脱敏是常见的安全需求。普通的基于工具类方法的方式,对代码的入侵性太强,编写起来又特别麻烦。
sensitive 提供了基于注解的方式,并且内置了常见的脱敏方式,便于开发。
日志脱敏
为了金融交易的安全性,国家强制规定对于以下信息是要日志脱敏的:
-
用户名
-
手机号
-
邮箱
-
银行卡号
-
密码
-
身份证号
持久化加密
存储的时候上面的信息都需要加密,密码为不可逆加密,其他为可逆加密。
类似的功能有很多。不在本系统的解决范围内。
特性
-
基于注解的日志脱敏。
-
可以自定义策略实现,策略生效条件。
-
内置常见的十几种脱敏内置方案。
-
java 深拷贝,且原始对象不用实现任何接口。
-
支持用户自定义注解。
-
支持基于 FastJSON 直接生成脱敏后的 json
变更日志
快速开始
环境准备
JDK 7+
Maven 3.x
maven 导入
<dependency> <groupId>com.github.houbb</groupId> <artifactId>sensitive-core</artifactId> <version>1.0.0</version> </dependency>
核心 api 简介
SensitiveUtil 工具类的核心方法列表如下:
| 序号 | 方法 | 参数 | 结果 | 说明 |
|---|---|---|---|---|
| 1 | desCopy() | 目标对象 | 深度拷贝脱敏对象 | 适应性更强 |
| 2 | desJson() | 目标对象 | 脱敏对象 json | 性能较好 |
| 3 | desCopyCollection() | 目标对象集合 | 深度拷贝脱敏对象集合 | |
| 4 | desJsonCollection() | 目标对象集合 | 脱敏对象 json 集合 |
定义对象
- UserAnnotationBean.java
通过注解,指定每一个字段的脱敏策略。
public class UserAnnotationBean { @SensitiveStrategyChineseName private String username; @SensitiveStrategyPassword private String password; @SensitiveStrategyPassport private String passport; @SensitiveStrategyIdNo private String idNo; @SensitiveStrategyCardId private String bandCardId; @SensitiveStrategyPhone private String phone; @SensitiveStrategyEmail private String email; @SensitiveStrategyAddress private String address; @SensitiveStrategyBirthday private String birthday; @SensitiveStrategyGps private String gps; @SensitiveStrategyIp private String ip; @SensitiveStrategyMaskAll private String maskAll; @SensitiveStrategyMaskHalf private String maskHalf; @SensitiveStrategyMaskRange private String maskRange; //Getter & Setter //toString() }
- 数据准备
构建一个最简单的测试对象:
UserAnnotationBean bean = new UserAnnotationBean(); bean.setUsername("张三"); bean.setPassword("123456"); bean.setPassport("CN1234567"); bean.setPhone("13066668888"); bean.setAddress("中国上海市浦东新区外滩18号"); bean.setEmail("whatanice@code.com"); bean.setBirthday("20220831"); bean.setGps("66.888888"); bean.setIp("127.0.0.1"); bean.setMaskAll("可恶啊我会被全部掩盖"); bean.setMaskHalf("还好我只会被掩盖一半"); bean.setMaskRange("我比较灵活指定掩盖范围"); bean.setBandCardId("666123456789066"); bean.setIdNo("360123202306018888");
- 测试代码
final String originalStr = "UserAnnotationBean{username='张三', password='123456', passport='CN1234567', idNo='360123202306018888', bandCardId='666123456789066', phone='13066668888', email='whatanice@code.com', address='中国上海市浦东新区外滩18号', birthday='20220831', gps='66.888888', ip='127.0.0.1', maskAll='可恶啊我会被全部掩盖', maskHalf='还好我只会被掩盖一半', maskRange='我比较灵活指定掩盖范围'}"; final String sensitiveStr = "UserAnnotationBean{username='张*', password='null', passport='CN*****67', idNo='3****************8', bandCardId='666123*******66', phone='1306****888', email='wh************.com', address='中国上海********8号', birthday='20*****1', gps='66*****88', ip='127***0.1', maskAll='**********', maskHalf='还好我只会*****', maskRange='我*********围'}"; final String expectSensitiveJson = "{"address":"中国上海********8号","bandCardId":"666123*******66","birthday":"20*****1","email":"wh************.com","gps":"66*****88","idNo":"3****************8","ip":"127***0.1","maskAll":"**********","maskHalf":"还好我只会*****","maskRange":"我*********围","passport":"CN*****67","phone":"1306****888","username":"张*"}"; UserAnnotationBean sensitiveUser = SensitiveUtil.desCopy(bean); Assert.assertEquals(sensitiveStr, sensitiveUser.toString()); Assert.assertEquals(originalStr, bean.toString()); String sensitiveJson = SensitiveUtil.desJson(bean); Assert.assertEquals(expectSensitiveJson, sensitiveJson);
我们可以直接利用 sensitiveUser 去打印日志信息,而这个对象对于代码其他流程不影响,我们依然可以使用原来的 user 对象。
当然,也可以使用 sensitiveJson 打印日志信息。
@Sensitive 注解
说明
@SensitiveStrategyChineseName 这种注解是为了便于用户使用,本质上等价于 @Sensitive(strategy = StrategyChineseName.class)。
@Sensitive 注解可以指定对应的脱敏策略。
内置注解与映射
| 编号 | 注解 | 等价 @Sensitive | 备注 |
|---|---|---|---|
| 1 | @SensitiveStrategyChineseName |
@Sensitive(strategy = StrategyChineseName.class) |
中文名称脱敏 |
| 2 | @SensitiveStrategyPassword |
@Sensitive(strategy = StrategyPassword.class) |
密码脱敏 |
| 3 | @SensitiveStrategyEmail |
@Sensitive(strategy = StrategyEmail.class) |
email 脱敏 |
| 4 | @SensitiveStrategyCardId |
@Sensitive(strategy = StrategyCardId.class) |
卡号脱敏 |
| 5 | @SensitiveStrategyPhone |
@Sensitive(strategy = StrategyPhone.class) |
手机号脱敏 |
| 6 | @SensitiveStrategyIdNo |
@Sensitive(strategy = StrategyIdNo.class) |
身份证脱敏 |
| 6 | @SensitiveStrategyAddress |
@Sensitive(strategy = StrategyAddress.class) |
地址脱敏 |
| 7 | @SensitiveStrategyGps |
@Sensitive(strategy = StrategyGps.class) |
GPS 脱敏 |
| 8 | @SensitiveStrategyIp |
@Sensitive(strategy = StrategyIp.class) |
IP 脱敏 |
| 9 | @SensitiveStrategyBirthday |
@Sensitive(strategy = StrategyBirthday.class) |
生日脱敏 |
| 10 | @SensitiveStrategyPassport |
@Sensitive(strategy = StrategyPassport.class) |
护照脱敏 |
| 11 | @SensitiveStrategyMaskAll |
@Sensitive(strategy = StrategyMaskAll.class) |
全部脱敏 |
| 12 | @SensitiveStrategyMaskHalf |
@Sensitive(strategy = StrategyMaskHalf.class) |
一半脱敏 |
| 13 | @SensitiveStrategyMaskRange |
@Sensitive(strategy = StrategyMaskRange.class) |
指定范围脱敏 |
@Sensitive 定义
@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Sensitive { /** * 注解生效的条件 * @return 条件对应的实现类 */ Class<? extends ICondition> condition() default ConditionAlwaysTrue.class; /** * 执行的策略 * @return 策略对应的类型 */ Class<? extends IStrategy> strategy(); }
与 @Sensitive 混合使用
如果你将新增的注解 @SensitiveStrategyChineseName 与 @Sensitive 同时在一个字段上使用。
为了简化逻辑,优先选择执行 @Sensitive,如果 @Sensitive 执行脱敏,
那么 @SensitiveStrategyChineseName 将不会生效。
如:
/** * 测试字段 * 1.当多种注解混合的时候,为了简化逻辑,优先选择 @Sensitive 注解。 */ @SensitiveStrategyChineseName @Sensitive(strategy = StrategyPassword.class) private String testField;
更多特性
自定义脱敏策略生效的场景
默认情况下,我们指定的场景都是生效的。
但是你可能需要有些情况下不进行脱敏,比如有些用户密码为 123456,你觉得这种用户不脱敏也罢。
- UserPasswordCondition.java
@Sensitive(condition = ConditionFooPassword.class, strategy = StrategyPassword.class) private String password;
其他保持不变,我们指定了一个 condition,实现如下:
- ConditionFooPassword.java
public class ConditionFooPassword implements ICondition { @Override public boolean valid(IContext context) { try { Field field = context.getCurrentField(); final Object currentObj = context.getCurrentObject(); final String password = (String) field.get(currentObj); return !password.equals("123456"); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } }
也就是只有当密码不是 123456 时密码脱敏策略才会生效。
属性为集合或者对象
如果某个属性是单个集合或者对象,则需要使用注解 @SensitiveEntry。
- 放在集合属性上,且属性为普通对象
会遍历每一个属性,执行上面的脱敏策略。
- 放在对象属性上
会处理对象中各个字段上的脱敏注解信息。
- 放在集合属性上,且属性为对象
遍历每一个对象,处理对象中各个字段上的脱敏注解信息。
放在集合属性上,且属性为普通对象
- UserEntryBaseType.java
作为演示,集合中为普通的字符串。
public class UserEntryBaseType { @SensitiveEntry @Sensitive(strategy = StrategyChineseName.class) private List<String> chineseNameList; @SensitiveEntry @Sensitive(strategy = StrategyChineseName.class) private String[] chineseNameArray; //Getter & Setter & toString() }
放在对象属性上
例子如下:
public class UserEntryObject { @SensitiveEntry private User user; @SensitiveEntry private List<User> userList; @SensitiveEntry private User[] userArray; //... }
自定义注解
- v0.0.4 新增功能。允许功能自定义条件注解和策略注解。
- v0.0.11 新增功能。允许功能自定义级联脱敏注解。
案例1
自定义密码脱敏策略&自定义密码脱敏策略生效条件
- 策略脱敏
/** * 自定义密码脱敏策略 * @author binbin.hou * date 2019/1/17 * @since 0.0.4 */ @Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @SensitiveStrategy(CustomPasswordStrategy.class) public @interface SensitiveCustomPasswordStrategy { }
- 脱敏生效条件
/** * 自定义密码脱敏策略生效条件 * @author binbin.hou * date 2019/1/17 * @since 0.0.4 */ @Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @SensitiveCondition(ConditionFooPassword.class) public @interface SensitiveCustomPasswordCondition{ }
- TIPS
@SensitiveStrategy 策略单独使用的时候,默认是生效的。
如果有 @SensitiveCondition 注解,则只有当条件满足时,才会执行脱敏策略。
@SensitiveCondition 只会对系统内置注解和自定义注解生效,因为 @Sensitive 有属于自己的策略生效条件。
- 策略优先级
@Sensitive 优先生效,然后是系统内置注解,最后是用户自定义注解。
对应的实现
两个元注解 @SensitiveStrategy、@SensitiveCondition 分别指定了对应的实现。
- CustomPasswordStrategy.java
public class CustomPasswordStrategy implements IStrategy { @Override public Object des(Object original, IContext context) { return "**********************"; } }
- ConditionFooPassword.java
/** * 让这些 123456 的密码不进行脱敏 * @author binbin.hou * date 2019/1/2 * @since 0.0.1 */ public class ConditionFooPassword implements ICondition { @Override public boolean valid(IContext context) { try { Field field = context.getCurrentField(); final Object currentObj = context.getCurrentObject(); final String name = (String) field.get(currentObj); return !name.equals("123456"); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } }
定义测试对象
定义一个使用自定义注解的对象。
public class CustomPasswordModel { @SensitiveCustomPasswordCondition @SensitiveCustomPasswordStrategy private String password; @SensitiveCustomPasswordCondition @SensitiveStrategyPassword private String fooPassword; //其他方法 }
测试
/** * 自定义注解测试 */ @Test public void customAnnotationTest() { final String originalStr = "CustomPasswordModel{password='hello', fooPassword='123456'}"; final String sensitiveStr = "CustomPasswordModel{password='**********************', fooPassword='123456'}"; CustomPasswordModel model = buildCustomPasswordModel(); Assert.assertEquals(originalStr, model.toString()); CustomPasswordModel sensitive = SensitiveUtil.desCopy(model); Assert.assertEquals(sensitiveStr, sensitive.toString()); Assert.assertEquals(originalStr, model.toString()); }
构建对象的方法如下:
/** * 构建自定义密码对象 * @return 对象 */ private CustomPasswordModel buildCustomPasswordModel(){ CustomPasswordModel model = new CustomPasswordModel(); model.setPassword("hello"); model.setFooPassword("123456"); return model; }
案例2
- v0.0.11 新增功能。允许功能自定义级联脱敏注解。
自定义级联脱敏注解
- 自定义级联脱敏注解
可以根据自己的业务需要,在自定义的注解上使用 @SensitiveEntry。
使用方式保持和 @SensitiveEntry 一样即可。
/** * 级联脱敏注解,如果对象中属性为另外一个对象(集合),则可以使用这个注解指定。 * <p> * 1. 如果属性为 Iterable 的子类集合,则当做列表处理,遍历其中的对象 * 2. 如果是普通对象,则处理对象中的脱敏信息 * 3. 如果是普通字段/MAP,则不做处理 * @since 0.0.11 */ @Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @SensitiveEntry public @interface SensitiveEntryCustom { }
定义测试对象
定义一个使用自定义注解的对象。
public class CustomUserEntryObject { @SensitiveEntryCustom private User user; @SensitiveEntryCustom private List<User> userList; @SensitiveEntryCustom private User[] userArray; // 其他方法... }
生成脱敏后的 JSON
说明
为了避免生成中间脱敏对象,v0.0.6 之后直接支持生成脱敏后的 JSON。
使用方法
新增工具类方法,可以直接返回脱敏后的 JSON。
生成的 JSON 是脱敏的,原对象属性值不受影响。
public static String desJson(Object object)
注解的使用方式
和 SensitiveUtil.desCopy() 完全一致。
使用示例代码
所有的测试案例中,都添加了对应的 desJson(Object) 测试代码,可以参考。
此处只展示最基本的使用。
final String originalStr = "SystemBuiltInAt{phone='18888888888', password='1234567', name='脱敏君', email='12345@qq.com', cardId='123456190001011234'}"; final String sensitiveJson = "{"cardId":"123456**********34","email":"12******.com","name":"脱**","phone":"1888****888"}"; SystemBuiltInAt systemBuiltInAt = DataPrepareTest.buildSystemBuiltInAt(); Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(systemBuiltInAt)); Assert.assertEquals(originalStr, systemBuiltInAt.toString());
注意
本次 JSON 脱敏基于 FastJSON。
FastJSON 在序列化本身存在一定限制。当对象中有集合,集合中还是对象时,结果不尽如人意。
示例代码
本测试案例可见测试代码。
final String originalStr = "UserCollection{userList=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userSet=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userCollection=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userMap={map=User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}}}"; final String commonJson = "{"userArray":[{"email":"12345@qq.com","idCard":"123456190001011234","password":"1234567","phone":"18888888888","username":"脱敏君"}],"userCollection":[{"$ref":"$.userArray[0]"}],"userList":[{"$ref":"$.userArray[0]"}],"userMap":{"map":{"$ref":"$.userArray[0]"}},"userSet":[{"$ref":"$.userArray[0]"}]}"; final String sensitiveJson = "{"userArray":[{"email":"12******.com","idCard":"123456**********34","phone":"1888****888","username":"脱**"}],"userCollection":[{"$ref":"$.userArray[0]"}],"userList":[{"$ref":"$.userArray[0]"}],"userMap":{"map":{"$ref":"$.userArray[0]"}},"userSet":[{"$ref":"$.userArray[0]"}]}"; UserCollection userCollection = DataPrepareTest.buildUserCollection(); Assert.assertEquals(commonJson, JSON.toJSONString(userCollection)); Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(userCollection)); Assert.assertEquals(originalStr, userCollection.toString());
解决方案
如果有这种需求,建议使用原来的 desCopy(Object)。
脱敏引导类
为了配置的灵活性,引入了引导类。
核心 api 简介
SensitiveBs 引导类的核心方法列表如下:
| 序号 | 方法 | 参数 | 结果 | 说明 |
|---|---|---|---|---|
| 1 | desCopy() | 目标对象 | 深度拷贝脱敏对象 | 适应性更强 |
| 2 | desJson() | 目标对象 | 脱敏对象 json | 性能较好 |
使用示例
使用方式和工具类一致,示意如下:
SensitiveBs.newInstance().desCopy(user);
配置深度拷贝实现
默认的使用 FastJson 进行对象的深度拷贝,等价于:
SensitiveBs.newInstance() .deepCopy(FastJsonDeepCopy.getInstance()) .desJson(user);
deepCopy 用于指定深度复制的具体实现,支持用户自定义。
深度复制(DeepCopy)
说明
深度复制可以保证我们日志输出对象脱敏,同时不影响正常业务代码的使用。
可以实现深度复制的方式有很多种,默认基于 fastjson 实现的。
为保证后续良性发展,v0.0.13 版本之后将深度复制接口抽离为单独的项目:
内置策略
目前支持 6 种基于序列化实现的深度复制,便于用户替换使用。
每一种都可以单独使用,保证依赖更加轻量。
自定义
为满足不同场景的需求,深度复制策略支持用户自定义。