文章目录
整体加解密思路:通过 MyBatis 的拦截器,在插入库前和查询结果后对有注解的字段,进行相应的加解密处理。
项目目录:
文件说明:
- @DbSafe:数据库安全注解
- DbSafeManager:
- 读取配置的包路径
- 获取包路径下所有
.class
文件的资源对象 - 递归遍历类及其父类的字段
- 过滤标注 @DbSafe 注解的目标字段
- 如果有,就要把这个类和对应的字段存入全局
ConcurrentHashMap<Class<?>, Set<Field>>
进行缓存
- MybatisInputInterceptor:
- 拦截并处理 ParameterHandler 接口的 setParameters 方法
- 获取原始参数对象,判断对象是否在全局
ConcurrentHashMap
中 - 从缓存中获取需要加密的字段,反射获取原始值,加密后写回字段。
- MybatisOutputInterceptor:
- 拦截 ResultSetHandler 类的 handleResultSets 方法
- 获取结果集对象,判断对象是否在全局
ConcurrentHashMap
中 - 从缓存中获取需要解密的字段,反射获取原始值,解密后写回字段。
- DbSafeAutoConfiguration:注入 DbSafeManager 、MybatisInputInterceptor 、MybatisOutputInterceptor
- 可以通过
@AutoConfiguration(after = MybatisPlusConfig.class)
自定义 MyBatis 配置,此处可选,本例没有使用。
- 可以通过
- DbSafeStrategy:通过 枚举类 的方式实现了加解密策略,最核心的思想就是函数式编程。这个 Function,它是
java.util.function
包下的。 - EncryptUtil:加解密工具类
- application.yml:
typeAliasesPackage: com.ah.ums.service.**.entity
:配置包路径mybatis-extends.db-safe.enable: true
:数据库字段加密 开关,考虑 Mybatis 的拦截器对性能有所消耗,所以在没有加解密的场景时可以关闭的。
心得:
- 如果想要简单,只需要@DbSafe、MybatisInputInterceptor、MybatisOutputInterceptor、DbSafeAutoConfiguration 即可完成需求。
- DbSafeManager:是为了启动时将标注的类字段进行缓存,增加运行效率
- DbSafeStrategy:策略枚举类结合函数式编程的方式可以应用到很多场景,增加代码维护性
- MybatisPlusConfig:自定义 MyBatis 配置,读取 yaml 类型配置文件
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.ah.ums.service.gasazjPos.entity.FailPayRecordEntity)。
- Resources.classForName(…):加载类到 JVM 中。
- getSafeFieldSetFromClazz(clazz):获取该类中需要加密的字段集合,若字段集合非空,将其存入 fieldCache。
4. 过滤无效类类型
if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
return fieldSet; // 直接返回空集合
}
跳过接口/内部类/匿名类,因为接口没有字段。
5. 递归遍历类及其父类的字段
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields(); // 获取当前类所有字段(包括私有)
fieldSet.addAll(Arrays.asList(fields)); // 添加到集合
clazz = clazz.getSuperclass(); // 向上遍历父类
}
6. 过滤标注 @DbSafe 注解的目标字段
fieldSet = fieldSet.stream()
.filter(field ->
field.isAnnotationPresent(DbSafe.class) && // 有@DbSafe注解
field.getType() == String.class // 字段类型为String
)
.collect(Collectors.toSet());
仅处理String类型字段,因为用于加密文本数据,只支持字符串。
7. 设置字段可访问
for (Field field : fieldSet) {
field.setAccessible(true); // 突破私有访问限制
}
即使字段是private的,后续代码也可通过反射读写。
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;
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
Object parameterObject = parameterHandler.getParameterObject();
if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
this.safeInputHandler(parameterObject);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {return Plugin.wrap(target, this);}
@Override
public void setProperties(Properties properties) {}
/**
* 对字段进行安全处理
*
* @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 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);
}
}
}
safeInputHandler 代码拆解:
实现了一个嵌套对象结构的敏感字段加密处理器,核心逻辑是通过 递归 遍历Map、List和对象字段,结合反射对标记字段进行加密。
1. 空对象处理
if (ObjectUtil.isNull(sourceObject)) return;
2. 处理Map类型
if (sourceObject instanceof Map<?, ?>) {
Map<String, Object> map = (Map<String, Object>) sourceObject;
new HashSet<>(map.values()).forEach(this::safeInputHandler); // 去重后递归处理Value
return;
}
- 去重优化:通过 new HashSet<>(map.values()) 对 Value 去重,避免重复处理相同对象(如多个 Key 指向同一个 Value)。
3. 处理List类型
if (sourceObject instanceof List<?>) {
List<Object> list = (List<Object>) sourceObject;
if (CollUtil.isEmpty(list)) return;
// 优化:若首个元素无需处理,直接跳过整个List
Object firstItem = list.get(0);
if (ObjectUtil.isNull(firstItem) ||
CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {
return;
}
list.forEach(this::safeInputHandler); // 递归处理每个元素
return;
}
4. 处理普通对象
// 检查是否为需要加密的类
Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());
if (ObjectUtil.isNull(fields)) return;
// 遍历加密字段并加密值
try {
for (Field field : fields) {
Object originalValue = field.get(sourceObject);
String strValue = Convert.toStr(originalValue); // 转换为字符串(可能丢失类型信息)
String encryptedValue = dbSafeManager.encrypt(strValue, field); // 加密
field.set(sourceObject, encryptedValue); // 反射设置字段值
}
} catch (Exception e) {
log.error("处理安全字段时出错", e);
}
- 加密逻辑:从缓存中获取需要加密的字段,反射获取原始值,加密后写回字段。
4. 输出拦截器 MybatisOutputInterceptor
Mybatis
输出拦截器,用于拦截 ResultSetHandler 类的 handleResultSets 方法。作用:返回字段值时调用解密
package com.chinaums.security.interceptor;
/**
* Mybatis出参拦截器
*/
@Slf4j
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
})
@AllArgsConstructor
public class MybatisOutputInterceptor implements Interceptor {
private final DbSafeManager dbSafeManager;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行mysql执行结果
Object result = invocation.proceed();
if (result == null) {
return null;
}
safeOutputHandler(result);
return result;
}
@Override
public Object plugin(Object target) {return Plugin.wrap(target, this);}
@Override
public void setProperties(Properties properties) {}
/**
* 输出对象
*
* @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);
}
}
}
5. 自动配置类
注入 DbSafeManager 、MybatisInputInterceptor 、MybatisOutputInterceptor
package com.chinaums.security.config;
/**
* 数据安全的配置类
*/
// @AutoConfiguration(after = MybatisPlusConfig.class)
@Configuration
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
@Slf4j
public class DbSafeAutoConfiguration {
@Bean
public DbSafeManager safeManager(MybatisPlusProperties mybatisPlusProperties) {
return new DbSafeManager(mybatisPlusProperties.getTypeAliasesPackage());
}
@Bean
public MybatisInputInterceptor inputInterceptor(DbSafeManager safeManager) {
return new MybatisInputInterceptor(safeManager);
}
@Bean
public MybatisOutputInterceptor outputInterceptor(DbSafeManager safeManager) {
return new MybatisOutputInterceptor(safeManager);
}
}
说明:
@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.**.vo
global-config:
dbConfig:
# 主键类型
# AUTO:自增; NONE:空; INPUT:用户输入;
# ASSIGN_ID:雪花(默认); ASSIGN_UUID: 唯一UUID
idType: ASSIGN_ID
👉引申:
MyBatis
的默认配置是在其 org.apache.ibatis.session.Configuration 类的构造方法中初始化的。当没有显式提供配置文件(如 mybatis-config.xml)时,MyBatis
会自动使用这些默认配置。
在Spring Boot
项目中,MyBatis
通常与 Spring Boot
的自动配置功能结合使用,读取 application.yml(或application.properties)中的配置。
也可以自定义配置文件进行 MyBatis
配置,示例如下:
@AutoConfiguration(after = MybatisPlusConfig.class)
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
@Slf4j
public class DbSafeAutoConfiguration { ... }
👉指定 DbSafeAutoConfiguration 需要在 MybatisPlusConfig 后面自动配置注入。
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 {
@Bean
public 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 的配置文件。
5.2 yaml 配置工厂
/**
* yaml配置工厂.可以将yaml文件解析为Properties
*/
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
/**
* 把yaml文件转换为属性源
*
* @param name 属性源的名称
* @param resource 资源
* @return 属性源
* @throws IOException IO异常
*/
@NonNull
@Override
public 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);
}
}
5.3 resources/default-mybatis-plus.yaml
默认配置的 yaml 文件,如下代码所示:
# Mybatis-Plus 默认配置
# 更多介绍 : https://baomidou.com/
mybatis-plus:
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
checkConfigLocation: false
configuration:
# 自动驼峰命名映射
mapUnderscoreToCamelCase: true
# MyBatis 自动映射策略
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
autoMappingBehavior: FULL
# MyBatis 自动映射时未知列或未知属性处理策
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 关闭日志记录 (使用p6spy进行日志输出)
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
banner: false
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID
insertStrategy: NOT_NULL
updateStrategy: NOT_NULL
whereStrategy: NOT_NULL
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")),
/**
* 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);
/**
* 加密器
*/
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;
}
}
通过枚举类的方式实现了加解密策略,最核心的思想就是函数式编程。这个 Function,它是 java.util.function
包下的。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
//......
}
这种函数式编程,最大的好处是:指定一个方法,去处理字符串,而具体怎么处理可以自己定义。比如我这里想定义 SM4 加解密算法。
ENCRYPT_SM4(s -> EncryptUtil.encryptBySm4(s, "ogb1iu59rfsk2y75"),
s -> EncryptUtil.decryptBySm4(s, "ogb1iu59rfsk2y75")),
调用:
(DbSafeStrategy 对象).decryptor().apply(value)
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);
}
}
8. application.yml
#mybatis
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.ah.ums.service.**.entity
global-config:
#数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: INPUT
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
insert-strategy: NOT_NULL
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
update-strategy: NOT_NULL
#驼峰下划线转换
column-underline: true
logic-delete-value: 1
logic-not-delete-value: 0
banner: false
#原生配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
# 数据库字段加密
mybatis-extends:
# 是否开启加密
db-safe:
enable: true
9. 使用
@Data
public class SysUserVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户账号
*/
private String userNo;
/**
* 用户名称密文
*/
@DbSafe
private String userName;
}