基于MyBatis自定义拦截器实现数据库字段加密脱敏
整体加解密的思路是:通过 MyBatis 的拦截器,在插入库前和查询结果后对有注解的字段,进行相应的处理。
1. 新增注解@DbSafe
package com.chinaums.security.annotation;
/*** 数据库安全注解* 支持字段的自动加解密和存入时脱敏*/
@Documented
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbSafe {/*** 安全策略*/DbSafeStrategy strategy() default DbSafeStrategy.ENCRYPT_SM4;boolean encrypt() default true;boolean decrypt() default true;
}
2. DbSafeManager
这个 Manager 的最主要判断类中是否含有 @DbSafe 注解的字段。如果有就要把这个类和对应的字段 缓存 起来,并且调用注解中策略进行加解密操作。
package com.chinaums.security.manager;
/*** 加密管理类*/
@Slf4j
@AllArgsConstructor
public class DbSafeManager {/*** 类加密字段缓存*/private Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();/*** 构造方法传入实体类包** @param typeAliasesPackage 实体类包*/public DbSafeManager(String typeAliasesPackage) {scanDbSafeClasses(typeAliasesPackage);}/*** 获取类加密字段缓存*/public Set<Field> getFieldCache(Class<?> sourceClazz) {if (ObjectUtil.isNotNull(fieldCache)) {return fieldCache.get(sourceClazz);}return null;}public String encrypt(String value, Field field) {DbSafe dbSafe = field.getAnnotation(DbSafe.class);return !dbSafe.encrypt() ? value : dbSafe.strategy().encryptor().apply(value);}public String decrypt(String value, Field field) {DbSafe dbSafe = field.getAnnotation(DbSafe.class);return !dbSafe.decrypt() ? value : dbSafe.strategy().decryptor().apply(value);}/*** 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体*/private void scanDbSafeClasses(String typeAliasesPackage) {PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();String splitRegex = "[:,]";String[] packagePatternArray = typeAliasesPackage.split(splitRegex);String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;try {for (String packagePattern : packagePatternArray) {String path = ClassUtils.convertClassNameToResourcePath(packagePattern);Resource[] resources = resolver.getResources(classpath + path + "/*.class");for (Resource resource : resources) {ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();Class<?> clazz = Resources.classForName(classMetadata.getClassName());Set<Field> encryptFieldSet = getSafeFieldSetFromClazz(clazz);if (CollUtil.isNotEmpty(encryptFieldSet)) {fieldCache.put(clazz, encryptFieldSet);}}}} catch (Exception e) {log.error("初始化数据安全缓存时出错:{}", e.getMessage());}}/*** 获得一个类的安全字段集合*/private Set<Field> getSafeFieldSetFromClazz(Class<?> clazz) {Set<Field> fieldSet = new HashSet<>();// 判断clazz如果是接口,内部类,匿名类就直接返回if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {return fieldSet;}while (clazz != null) {Field[] fields = clazz.getDeclaredFields();fieldSet.addAll(Arrays.asList(fields));clazz = clazz.getSuperclass();}fieldSet = fieldSet.stream().filter(field ->field.isAnnotationPresent(DbSafe.class) && field.getType() == String.class).collect(Collectors.toSet());for (Field field : fieldSet) {field.setAccessible(true);}return fieldSet;}}
代码拆解:
1. 处理包路径配置
String splitRegex = "[:,]";
String[] packagePatternArray = typeAliasesPackage.split(splitRegex);
我们配置的 typeAliasesPackage 是 com.ah.ums.service.**.entity
2. 类路径扫描
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX; // classpath: classpath*:
String path = ClassUtils.convertClassNameToResourcePath(packagePattern); // path: com/ah/ums/service/**/entity
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
- PathMatchingResourcePatternResolver resolver:用于解析类路径资源,支持Ant风格路径匹配(如
**/*.class
)。 - ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX:类路径前缀
classpath*:
- ClassUtils.convertClassNameToResourcePath(packagePattern):将包名转换为文件路径(如
com.ah.ums.service.**.entity
→com/ah/ums/service/**/entity
)。 - resolver.getResources(classpath + path + “/*.class”):获取包路径下所有
.class
文件的资源对象。
3. 类元数据读取与处理
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
for (Resource resource : resources) {ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();Class<?> clazz = Resources.classForName(classMetadata.getClassName());Set<Field> encryptFieldSet = getSafeFieldSetFromClazz(clazz);if (CollUtil.isNotEmpty(encryptFieldSet)) {fieldCache.put(clazz, encryptFieldSet);}
}
-
CachingMetadataReaderFactory factory:用于读取类元数据(如类名、方法等)并缓存,提升性能。
-
factory.getMetadataReader(resource):读取类元数据(无需加载类)。
-
classMetadata.getClassName():获取完整类名(如com.example.model.User)。
-
Resources.classForName(…):加载类到JVM中。
-
getSafeFieldSetFromClazz(clazz):获取该类中需要加密的字段集合,若字段集合非空,将其存入fieldCache。
7.3 输入拦截器 MybatisInputInterceptor
这里通过使用 Mybatis
框架的拦截器实现对字段的加密动作,这里拦截并处理 ParameterHandler 接口的setParameters 方法。作用:设置参数值时调用加密
package com.chinaums.security.interceptor;
/*** Mybatis入参拦截器**/
@Slf4j
@Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})
})
@AllArgsConstructor
public class MybatisInputInterceptor implements Interceptor {private final DbSafeManager dbSafeManager;@Overridepublic Object intercept(Invocation invocation) {return invocation;}@Overridepublic Object plugin(Object target) {if (target instanceof ParameterHandler) {// 进行加密操作ParameterHandler parameterHandler = (ParameterHandler) target;Object parameterObject = parameterHandler.getParameterObject();if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {this.safeInputHandler(parameterObject);}}return target;}/*** 对字段进行安全处理** @param sourceObject 原对象*/private void safeInputHandler(Object sourceObject) {if (ObjectUtil.isNull(sourceObject)) {return;}if (sourceObject instanceof Map<?, ?>) {Map<String, Object> map = (Map<String, Object>) sourceObject;new HashSet<>(map.values()).forEach(this::safeInputHandler);return;}if (sourceObject instanceof Map<?, ?>) {Map<String, Object> map = (Map<String, Object>) sourceObject;map.values().forEach(this::safeInputHandler);}if (sourceObject instanceof List<?>) {List<Object> list = (List<Object>) sourceObject;if(CollUtil.isEmpty(list)) {return;}// 判断第一个元素是否含有注解。如果没有直接返回,提高效率Object firstItem = list.get(0);if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {return;}list.forEach(this::safeInputHandler);return;}// 是不是在缓存中的需要数据安全的类Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());if(ObjectUtil.isNull(fields)){return;}try {for (Field field : fields) {field.set(sourceObject, this.dbSafeManager.encrypt(Convert.toStr(field.get(sourceObject)), field));}} catch (Exception e) {log.error("处理安全字段时出错", e);}}
}
7.4 输出拦截器 MybatisOutputInterceptor
Mybatis
输出拦截器,用于拦截 ResultSetHandler 类的 handleResultSets 方法。作用:返回字段值时调用解密
package com.chinaums.security.interceptor;
/*** Mybatis出参拦截器*/
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class,method = "update",args = {Statement.class})
})
@AllArgsConstructor
public class MybatisOutputInterceptor implements Interceptor {private final DbSafeManager dbSafeManager;@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取执行mysql执行结果Object result = invocation.proceed();if (result == null) {return null;}safeOutputHandler(result);return result;}/*** 输出对象** @param sourceObject 待输出对象*/private void safeOutputHandler(Object sourceObject) {if (ObjectUtil.isNull(sourceObject)) {return;}if (sourceObject instanceof Map<?, ?>) {Map<?, ?> map = (Map<?, ?>) sourceObject;new HashSet<>(map.values()).forEach(this::safeOutputHandler);return;}if (sourceObject instanceof List<?>) {List<?> list = (List<?>) sourceObject;if (CollUtil.isEmpty(list)) {return;}// 判断第一个元素是否含有注解。如果没有直接返回,提高效率Object firstItem = list.get(0);if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {return;}list.forEach(this::safeOutputHandler);return;}Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());if (ObjectUtil.isNull(fields)) {return;}try {for (Field field : fields) {field.set(sourceObject, this.dbSafeManager.decrypt(Convert.toStr(field.get(sourceObject)), field));}} catch (Exception e) {log.error("处理安全字段时出错", e);}}
}
7.5 增加数据安全的配置类
package com.chinaums.security.config;
/*** 数据安全的配置类*/
@AutoConfiguration(after = MybatisPlusConfig.class)
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
@Slf4j
public class DbSafeAutoConfiguration {@Beanpublic DbSafeManager safeManager(MybatisPlusProperties mybatisPlusProperties) {return new DbSafeManager(mybatisPlusProperties.getTypeAliasesPackage());}@Beanpublic MybatisInputInterceptor inputInterceptor(DbSafeManager safeManager) {return new MybatisInputInterceptor(safeManager);}@Beanpublic MybatisOutputInterceptor outputInterceptor(DbSafeManager safeManager) {return new MybatisOutputInterceptor(safeManager);}/*** 数据权限拦截器*/@Beanpublic PlusDataPermissionInterceptor dataPermissionInterceptor() {PlusDataPermissionInterceptor dataPermissionInterceptor = new PlusDataPermissionInterceptor();MybatisPlusInterceptor mybatisPlusInterceptor = SpringUtils.getBean("mybatisPlusInterceptor");mybatisPlusInterceptor.addInnerInterceptor(dataPermissionInterceptor);return dataPermissionInterceptor;}}
说明:
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
这里考虑 Mybatis 的拦截器对性能有所消耗,所以在没有加解密的场景时可以关闭的。对应在 application.yml 的配置为:
# 数据库字段加密
mybatis-extends:# 是否开启加密db-safe:enable: true
当 enable 为 true 时,该配置类才会被执行,这里的3个Bean才会被创建出来。
👉mybatisPlusProperties.getTypeAliasesPackage() 获取 application.yml 文件中 typeAliasesPackage
# Mybatis Plus配置
mybatis-plus:# mapper XML 文件位置mapperLocations: classpath*:mapper/**/*Mapper.xml# 实体扫描,多个package用逗号或者分号分隔typeAliasesPackage: com.chinaums.capacity.**.entity,com.chinaums.capacity.**.voglobal-config:dbConfig:# 主键类型# AUTO:自增; NONE:空; INPUT:用户输入;# ASSIGN_ID:雪花(默认); ASSIGN_UUID: 唯一UUIDidType: ASSIGN_ID
👉引申:
MyBatis
的默认配置是在其 org.apache.ibatis.session.Configuration 类的构造方法中初始化的。当没有显式提供配置文件(如 mybatis-config.xml)时,MyBatis
会自动使用这些默认配置。
在Spring Boot
项目中,MyBatis
通常与 Spring Boot
的自动配置功能结合使用,读取 application.yml(或application.properties)中的配置。
也可以自定义配置文件进行 MyBatis
配置,示例如下:
7.5.1 mybatis-plus配置类 MybatisPlusConfig
package com.chinaums.database.config;
/*** mybatis-plus配置类*/
@EnableTransactionManagement(proxyTargetClass = true)
@AutoConfiguration
@PropertySource(value = "classpath:default-mybatis-plus.yaml", factory = YmlPropertySourceFactory.class)
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(paginationInnerInterceptor());// 乐观锁插件interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());// 阻断插件interceptor.addInnerInterceptor(blockAttackInnerInterceptor());return interceptor;}/*** 分页插件,自动识别数据库类型*/private PaginationInnerInterceptor paginationInnerInterceptor() {PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInnerInterceptor.setMaxLimit(-1L);// 溢出页面返回第一页paginationInnerInterceptor.setOverflow(true);return paginationInnerInterceptor;}/*** 乐观锁插件*/private OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {return new OptimisticLockerInnerInterceptor();}/*** 阻断插件(防删库跑路插件.如果是对全表的删除或更新操作,就会终止该操作)*/private BlockAttackInnerInterceptor blockAttackInnerInterceptor() {return new BlockAttackInnerInterceptor();}
}
对于MP的默认设置,不太需要开发人员进行配置。这里的默认配置是应该作为一个属性源,直接加载到 springboot 中的,但是 springboot 默认只支持 Properties 的属性源,所以这里需要一个能支持 yaml 格式的属性源工厂 YmlPropertySourceFactory 替换掉默认属性源工厂。
MP的默认设置,这样需要在模块中加入一个 default-mybatis-plus.yaml 的配置文件。
7.5.2 yaml 配置工厂
/*** yaml配置工厂.可以将yaml文件解析为Properties*/
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {/*** 把yaml文件转换为属性源** @param name 属性源的名称* @param resource 资源* @return 属性源* @throws IOException IO异常*/@NonNull@Overridepublic PropertySource<?> createPropertySource(@Nullable String name, @NonNull EncodedResource resource) throws IOException {String sourceName = resource.getResource().getFilename();if (StrUtil.isNotBlank(sourceName) && StrUtil.endWithAny(sourceName, ".yml", ".yaml")) {YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();factory.setResources(resource.getResource());factory.afterPropertiesSet();return new PropertiesPropertySource(sourceName, Objects.requireNonNull(factory.getObject()));}return super.createPropertySource(name, resource);}
}
7.5.3 resources/default-mybatis-plus.yaml
默认配置的 yaml 文件,如下代码所示:
# Mybatis-Plus 默认配置
# 更多介绍 : https://baomidou.com/
mybatis-plus:# 启动时是否检查 MyBatis XML 文件的存在,默认不检查checkConfigLocation: falseconfiguration:# 自动驼峰命名映射mapUnderscoreToCamelCase: true# MyBatis 自动映射策略# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射autoMappingBehavior: FULL# MyBatis 自动映射时未知列或未知属性处理策# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息autoMappingUnknownColumnBehavior: NONE# 关闭日志记录 (使用p6spy进行日志输出)# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpllogImpl: org.apache.ibatis.logging.nologging.NoLoggingImplglobal-config:# 是否打印 Logo bannerbanner: falsedbConfig:# 主键类型# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUIDidType: ASSIGN_IDinsertStrategy: NOT_NULLupdateStrategy: NOT_NULLwhereStrategy: NOT_NULL
7.6 加解密策略枚举 DbSafeStrategy
package com.chinaums.security.strategy;
/*** 数据库加解密策略*/
@AllArgsConstructor
public enum DbSafeStrategy {/*** SM2 加密算法* 可通过EncryptUtil.generateSm2Key()方法自行生成公钥和私钥*/ENCRYPT_SM2(s -> EncryptUtil.encryptBySm2(s, "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEik1NRz0PJ4xe+puBGfeCn44lNyjUyaNkza/wIdwBT0fjywVqauLeav3PSLF0/pKmTedrR6QJD/3w62yCYS1OTQ=="),s -> EncryptUtil.decryptBySm2(s, "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgnCoYlS6/lCPDfWjNJyQozC0EkdKYorTCOb+xC4tsatugCgYIKoEcz1UBgi2hRANCAASKTU1HPQ8njF76m4EZ94KfjiU3KNTJo2TNr/Ah3AFPR+PLBWpq4t5q/c9IsXT+kqZN52tHpAkP/fDrbIJhLU5N")),/*** SM3 加密算法*/ENCRYPT_SM3(EncryptUtil::encryptBySm3, s -> s),/*** SM4 加密算法* 秘钥字符串为16位长度任意英文字符串,可通过RandomUtil.randomString(16)方法生成*/ENCRYPT_SM4(s -> EncryptUtil.encryptBySm4(s, "ogb1iu59rfsk2y75"),s -> EncryptUtil.decryptBySm4(s, "ogb1iu59rfsk2y75")),/*** 公司内部sm4 A001*/ENCRYPT_UMS_SM4((s) -> UmsSM4Tools.enc(IndexTypeEnum.NAME, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.NAME, s)),/*** 公司内部sm4 姓名:A001*/ENCRYPT_UMS_SM4_NAME((s) -> UmsSM4Tools.enc(IndexTypeEnum.NAME, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.NAME, s)),/*** 公司内部sm4 卡号:B001*/ENCRYPT_UMS_SM4_PAN((s) -> UmsSM4Tools.enc(IndexTypeEnum.PAN, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.PAN, s)),/*** 公司内部sm4 身份证号(含护照、港澳通行证):C001*/ENCRYPT_UMS_SM4_ID_CARD((s) -> UmsSM4Tools.enc(IndexTypeEnum.ID_CARD, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.ID_CARD, s)),/*** 公司内部sm4 手机号码/固定电话号码:D001*/ENCRYPT_UMS_SM4_TEL((s) -> UmsSM4Tools.enc(IndexTypeEnum.TEL, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.TEL, s)),/*** 公司内部sm4 地址:E001*/ENCRYPT_UMS_SM4_ADDRESS((s) -> UmsSM4Tools.enc(IndexTypeEnum.ADDRESS, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.ADDRESS, s)),/*** 公司内部sm4 磁道信息:F001*/ENCRYPT_UMS_SM4_BIN((s) -> UmsSM4Tools.enc(IndexTypeEnum.BIN, s),(s) -> UmsSM4Tools.dec(IndexTypeEnum.BIN, s)),/*** AES 加密算法* 秘钥字符串为16位、24位、32位长度任意英文字符串,可通过RandomUtil.randomString(32)方法生成*/ENCRYPT_AES(s -> EncryptUtil.encryptByAes(s, "kj6j1vgq5nj3biph42jnqn2yre70accm"),s -> EncryptUtil.decryptByAes(s, "kj6j1vgq5nj3biph42jnqn2yre70accm")),/*** RSA 加密算法* 可通过EncryptUtil.generateRsaKey()方法自行生成公钥和私钥*/ENCRYPT_RSA(s -> EncryptUtil.encryptByRsa(s, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0ovKFQIwWK+ueJUxN1SSQKrlbegKQ7r9msu8Ar4hVD5f6cG/qZ4iv5QveVeiUAg499CDa1448aJXi2eqkmODeM6Si65BHdPH1urmL4O1uEysaIKquztiPsVix9E8azmK08balqXYbE7ybBQSIQ+yEXUJ+3M+WwmxK2lxZica8ywIDAQAB"),s -> EncryptUtil.decryptByRsa(s, "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALSi8oVAjBYr654lTE3VJJAquVt6ApDuv2ay7wCviFUPl/pwb+pniK/lC95V6JQCDj30INrXjjxoleLZ6qSY4N4zpKLrkEd08fW6uYvg7W4TKxogqq7O2I+xWLH0TxrOYrTxtqWpdhsTvJsFBIhD7IRdQn7cz5bCbEraXFmJxrzLAgMBAAECgYA7tiG1KsEkEyCwBmRS1kJf5b+gHZT7k/BxYnTfJSdL9vumLcTRF6h3fJ+Pv5ZCVuueTzUNInRCQ9BITQDjqCWsvnbDXQ3QHHwNy1ehK/DPgRbTYA9gvxLoLt6xRYuvPTPWFRwJNQCCIpsOKu/efQjb6xDMRUWKiXmBwt1nObnHAQJBAPjFFwcRcI1aWSXNrm+B4oHjKqJmHBeZ/inHLhOoheHTtqTMcjb1Q8jyurEjkFNWScIVOnBe1QKdpZJQaoY9FlUCQQC54u2Yqit8b2hPqGiOUwUd37ScfIcza3syyLjEU4YSVgtt+hpJ7Xi4e2dAne0zDW0Kk9hkqs2sGKDJYUYXMGafAkEA1uc0AGghagsdrhmj0jJLIVfEEezR4dWnCiJF/Ld9iNujEXSISk/QkfyWKMaHPGbzatV52W8i5pKXYPFVRMfqzQJAKVe/YGT4pwRgPtdF6eGtEaffk65eo6EUFYdvELtC5nEcuakWj7qxTtajcEuvpdsmlWOsjTcv50bS+/cWj7HEIQJAN2aZOY2v0/jymrObUcsH+d9VPQFd15CSdjJCTJ764aupj0tslsnm36wQq5eDn9PxmzGWOiwZYDBXIXVF3Fq9Eg==")),/*** SHA256 加密算法*/ENCRYPT_SHA256(EncryptUtil::encryptBySha256, s -> s),/*** MD5 加密算法*/ENCRYPT_MD5(EncryptUtil::encryptByMd5, s -> s),/脱敏加密分割线//*** 身份证脱敏, 前6后4显示*/DESENSITIZE_ID_CARD(s -> DesensitizedUtil.idCardNum(s, 6, 4), s -> s),/*** 手机号脱敏.前三位,后4位,其他隐藏,比如135****2210*/DESENSITIZE_MOBILE_PHONE(DesensitizedUtil::mobilePhone, s -> s),/*** 邮箱脱敏.* 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com*/DESENSITIZE_EMAIL(DesensitizedUtil::email, s -> s),/*** 银行卡号脱敏* 由于银行卡号长度不定,所以只展示前4位,后面的位数根据卡号决定展示1-4位 例如:* 1. "1234 2222 3333 4444 6789 9" -> "1234 **** **** **** **** 9"* 2. "1234 2222 3333 4444 6789 91" -> "1234 **** **** **** **** 91"* 3. "1234 2222 3333 4444 678" -> "1234 **** **** **** 678"* 4. "1234 2222 3333 4444 6789" -> "1234 **** **** **** 6789"*/DESENSITIZE_BANK_CARD(DesensitizedUtil::bankCard, s -> s),/*** 中文姓名脱敏* 只显示第一个汉字,其他隐藏为2个星号,比如:李***/DESENSITIZE_CHINESE_NAME(DesensitizedUtil::chineseName, s -> s);/*** 加密器*/private final Function<String, String> encryptor;/*** 解密器*/private final Function<String, String> decryptor;/*** 获得具体的脱敏器** @return 具体的脱敏器*/public Function<String, String> encryptor() {return encryptor;}public Function<String, String> decryptor() {return decryptor;}
}
7.7 加解密工具类
package com.chinaums.security.util;
/*** 常用加密工具类*/
public class EncryptUtil {/*** 公钥*/public static final String PUBLIC_KEY = "publicKey";/*** 私钥*/public static final String PRIVATE_KEY = "privateKey";/*** Base64加密** @param data 待加密数据* @return 加密后字符串*/public static String encryptByBase64(String data) {return Base64.encode(data, StandardCharsets.UTF_8);}/*** Base64解密** @param data 待解密数据* @return 解密后字符串*/public static String decryptByBase64(String data) {return Base64.decodeStr(data, StandardCharsets.UTF_8);}/*** AES加密** @param data 待解密数据* @param password 秘钥字符串* @return 加密后字符串, 采用Base64编码*/public static String encryptByAes(String data, String password) {if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("AES需要传入秘钥信息");}// aes算法的秘钥要求是16位、24位、32位int[] array = {16, 24, 32};if (!ArrayUtil.contains(array, password.length())) {throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");}return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);}/*** AES加密** @param data 待解密数据* @param password 秘钥字符串* @return 加密后字符串, 采用Hex编码*/public static String encryptByAesHex(String data, String password) {if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("AES需要传入秘钥信息");}// aes算法的秘钥要求是16位、24位、32位int[] array = {16, 24, 32};if (!ArrayUtil.contains(array, password.length())) {throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");}return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);}/*** AES解密** @param data 待解密数据* @param password 秘钥字符串* @return 解密后字符串*/public static String decryptByAes(String data, String password) {if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("AES需要传入秘钥信息");}// aes算法的秘钥要求是16位、24位、32位int[] array = {16, 24, 32};if (!ArrayUtil.contains(array, password.length())) {throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");}return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);}/*** sm4加密** @param data 待加密数据* @param password 秘钥字符串* @return 加密后字符串, 采用Base64编码*/public static String encryptBySm4(String data, String password) {if (StrUtil.isBlank(data)) {return data;}if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("SM4需要传入秘钥信息");}// sm4算法的秘钥要求是16位长度int sm4PasswordLength = 16;if (sm4PasswordLength != password.length()) {throw new IllegalArgumentException("SM4秘钥长度要求为16位");}return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);}/*** sm4加密** @param data 待加密数据* @param password 秘钥字符串* @return 加密后字符串, 采用Base64编码*/public static String encryptBySm4Hex(String data, String password) {if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("SM4需要传入秘钥信息");}// sm4算法的秘钥要求是16位长度int sm4PasswordLength = 16;if (sm4PasswordLength != password.length()) {throw new IllegalArgumentException("SM4秘钥长度要求为16位");}return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);}/*** sm4解密** @param data 待解密数据* @param password 秘钥字符串* @return 解密后字符串*/public static String decryptBySm4(String data, String password) {if (StrUtil.isBlank(data)) {return data;}if (StrUtil.isBlank(password)) {throw new IllegalArgumentException("SM4需要传入秘钥信息");}// sm4算法的秘钥要求是16位长度int sm4PasswordLength = 16;if (sm4PasswordLength != password.length()) {throw new IllegalArgumentException("SM4秘钥长度要求为16位");}return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);}/*** 产生sm2加解密需要的公钥和私钥** @return 公私钥Map*/public static Map<String, String> generateSm2Key() {Map<String, String> keyMap = new HashMap<>(2);SM2 sm2 = SmUtil.sm2();keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());return keyMap;}/*** sm2公钥加密** @param data 待加密数据* @param publicKey 公钥* @return 加密后字符串, 采用Base64编码*/public static String encryptBySm2(String data, String publicKey) {if (StrUtil.isBlank(publicKey)) {throw new IllegalArgumentException("SM2需要传入公钥进行加密");}SM2 sm2 = SmUtil.sm2(null, publicKey);return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);}/*** sm2公钥加密** @param data 待加密数据* @param publicKey 公钥* @return 加密后字符串, 采用Hex编码*/public static String encryptBySm2Hex(String data, String publicKey) {if (StrUtil.isBlank(publicKey)) {throw new IllegalArgumentException("SM2需要传入公钥进行加密");}SM2 sm2 = SmUtil.sm2(null, publicKey);return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);}/*** sm2私钥解密** @param data 待加密数据* @param privateKey 私钥* @return 解密后字符串*/public static String decryptBySm2(String data, String privateKey) {if (StrUtil.isBlank(privateKey)) {throw new IllegalArgumentException("SM2需要传入私钥进行解密");}SM2 sm2 = SmUtil.sm2(privateKey, null);return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);}/*** 产生RSA加解密需要的公钥和私钥** @return 公私钥Map*/public static Map<String, String> generateRsaKey() {Map<String, String> keyMap = new HashMap<>(2);RSA rsa = SecureUtil.rsa();keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());return keyMap;}/*** rsa公钥加密** @param data 待加密数据* @param publicKey 公钥* @return 加密后字符串, 采用Base64编码*/public static String encryptByRsa(String data, String publicKey) {if (StrUtil.isBlank(publicKey)) {throw new IllegalArgumentException("RSA需要传入公钥进行加密");}RSA rsa = SecureUtil.rsa(null, publicKey);return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);}/*** rsa公钥加密** @param data 待加密数据* @param publicKey 公钥* @return 加密后字符串, 采用Hex编码*/public static String encryptByRsaHex(String data, String publicKey) {if (StrUtil.isBlank(publicKey)) {throw new IllegalArgumentException("RSA需要传入公钥进行加密");}RSA rsa = SecureUtil.rsa(null, publicKey);return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);}/*** rsa私钥解密** @param data 待加密数据* @param privateKey 私钥* @return 解密后字符串*/public static String decryptByRsa(String data, String privateKey) {if (StrUtil.isBlank(privateKey)) {throw new IllegalArgumentException("RSA需要传入私钥进行解密");}RSA rsa = SecureUtil.rsa(privateKey, null);return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);}/*** md5加密** @param data 待加密数据* @return 加密后字符串, 采用Hex编码*/public static String encryptByMd5(String data) {return SecureUtil.md5(data);}/*** sha256加密** @param data 待加密数据* @return 加密后字符串, 采用Hex编码*/public static String encryptBySha256(String data) {return SecureUtil.sha256(data);}/*** sm3加密** @param data 待加密数据* @return 加密后字符串, 采用Hex编码*/public static String encryptBySm3(String data) {return SmUtil.sm3(data);}}
7.7 使用
@Data
public class SysUserVo implements Serializable {private static final long serialVersionUID = 1L;/*** 用户ID*/private Long userId;/*** 用户账号*/private String userNo;/*** 用户名称密文*/@DbSafeprivate String userName;
}