MyBatis-Plus 提供了数据安全保护功能,旨在防止因开发人员流动而导致的敏感信息泄露。从3.3.2版本开始,MyBatis-Plus 支持通过加密配置和数据安全措施来增强数据库的安全性。
当然也可以使用Nacos,Apollo这种配置中心来管理。
配置加密
YML 配置加密
MyBatis-Plus 允许你使用加密后的字符串来配置数据库连接信息。在 YML 配置文件中,以 mpw:
开头的配置项将被视为加密内容。
spring:
datasource:
url: mpw:qRhvCwF4GOqjessEB3G+a5okP+uXXr96wcucn2Pev6Bf1oEMZ1gVpPPhdDmjQqoM
password: mpw:Hzy5iliJbwDHhjLs1L0j6w==
username: mpw:Xb+EgsyuYRXw7U7sBJjBpA==
使用 AES 算法生成随机密钥,并对敏感数据进行加密。
// 生成16位随机AES密钥
String randomKey = AES.generateRandomKey();
// 使用随机密钥加密数据
String encryptedData = AES.encrypt(data, randomKey);
package com.example.demo.utils;
import com.baomidou.mybatisplus.core.toolkit.AES;
public class AesUtils {
public static void main(String[] args) {
// 生成16位随机AES密钥
// String randomKey = AES.generateRandomKey();
String randomKey = "IumOgNjljMjgAPRU";
System.out.println(randomKey);
// 使用随机密钥加密数据
String encryptedData = AES.encrypt("jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&nullCatalogMeansCurrent=true&allowMultiQueries=true&allowPublicKeyRetrieval=true", randomKey);
String root = AES.encrypt("root", randomKey);
String pw = AES.encrypt("123456", randomKey);
System.out.println(encryptedData);
System.out.println(root);
System.out.println(pw);
}
}
生成加密数据
密钥:IumOgNjljMjgAPRU
地址:XmLADrrUhBmMZdAt+ojd5yJ9vNRDu1/kgRO0qHmXmvHOrmL+qz55GVEQARIHW1aJLKt+d0iwE+3wpwPaB+Ha0/3fMbrk8g0RIHb7FTp09xLs4iVXmg1nlc5m+rkLkDmZL4u+nNTsfNuDkYEM05bLVTUkpYIDuQNvVAXAlK2f+/h0F0SIqbasija42+zq+odGJaTmea4iuI9d97kalFkKjHyiBL3cqCRZJIkXQctnqjKKp5CKjnzyMQXyrK2TOSd9jtbqxRx0uSSvlrYSR0wgNAmVvINJk53Z8sh3tZPtJb0=
用户:CFeI7CB8lrga3NXlsefIpQ==
密码:tQ4Azxb1HPk6M0C6gkPOIA==
使用
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: mpw:XmLADrrUhBmMZdAt+ojd5yJ9vNRDu1/kgRO0qHmXmvHOrmL+qz55GVEQARIHW1aJLKt+d0iwE+3wpwPaB+Ha0/3fMbrk8g0RIHb7FTp09xLs4iVXmg1nlc5m+rkLkDmZL4u+nNTsfNuDkYEM05bLVTUkpYIDuQNvVAXAlK2f+/h0F0SIqbasija42+zq+odGJaTmea4iuI9d97kalFkKjHyiBL3cqCRZJIkXQctnqjKKp5CKjnzyMQXyrK2TOSd9jtbqxRx0uSSvlrYSR0wgNAmVvINJk53Z8sh3tZPtJb0=
username: mpw:CFeI7CB8lrga3NXlsefIpQ==
password: mpw:tQ4Azxb1HPk6M0C6gkPOIA==
spring boot 启动需要在启动指令加上--mpw.key=IumOgNjljMjgAPRU 指令启动。
idea直接启动在Program arguments中配置。
jar包启动是在 nohup java --mpw.key=IumOgNjljMjgAPRU -jar 加入。
有的idea版本可能没有Program arguments
但是 还是建议使用配置中心进行维护配置文件。
数据安全
MyBatis-Plus 关于数据加密和数据脱敏在社区版是没有的,需要购买企业版才有。我们可以使用mybatis拦截器方式做数据库字段加密,脱敏处理。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 加密字段注解
* 用于标记需要加密的字段
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptedField {
}
/**
* 数据加密拦截器
* 该拦截器用于拦截MyBatis的执行器(Executor)的update和query方法,以实现数据加密和解密
* 主要功能包括:
* 1. 在执行插入或更新操作前,对带有 @EncryptedField 注解的字段进行加密
* 2. 在执行查询操作后,对查询结果中带有 @EncryptedField 注解的字段进行解密
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataEncryptionInterceptor implements Interceptor {
/**
* 拦截方法的执行
* 在执行插入或更新操作时,对带有 @EncryptedField 注解的字段进行加密
* 在执行查询操作时,对查询结果中带有 @EncryptedField 注解的字段进行解密
*
* @param invocation 调用信息,包含方法、参数等信息
* @return 加密或解密后的结果
* @throws Throwable 如果执行过程中出现异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 处理插入或更新操作
if ("update".equals(invocation.getMethod().getName())) {
encryptFields(parameter);
}
// 处理查询操作
if ("query".equals(invocation.getMethod().getName())) {
Object result = invocation.proceed();
if (result instanceof List) {
decryptFields((List<?>) result);
}
return result;
}
return invocation.proceed();
}
/**
* 对带有 @EncryptedField 注解的字段进行加密
*
* @param object 需要加密的对象
*/
private void encryptFields(Object object) {
MetaObject metaObject = SystemMetaObject.forObject(object);
for (String fieldName : metaObject.getGetterNames()) {
if (metaObject.hasGetter(fieldName) && metaObject.getSetter(fieldName, String.class) != null) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
if (field.isAnnotationPresent(EncryptedField.class)) {
String value = metaObject.getValue(fieldName).toString();
String encryptedValue = EncryptionUtil.encrypt(value);
metaObject.setValue(fieldName, encryptedValue);
}
} catch (NoSuchFieldException e) {
// 忽略不存在的字段
}
}
}
}
/**
* 对查询结果中带有 @EncryptedField 注解的字段进行解密
*
* @param resultList 查询结果列表
*/
private void decryptFields(List<?> resultList) {
for (Object item : resultList) {
MetaObject metaObject = SystemMetaObject.forObject(item);
for (String fieldName : metaObject.getGetterNames()) {
if (metaObject.hasGetter(fieldName) && metaObject.getSetter(fieldName, String.class) != null) {
try {
Field field = item.getClass().getDeclaredField(fieldName);
if (field.isAnnotationPresent(EncryptedField.class)) {
String value = metaObject.getValue(fieldName).toString();
String decryptedValue = EncryptionUtil.decrypt(value);
metaObject.setValue(fieldName, decryptedValue);
}
} catch (NoSuchFieldException e) {
// 忽略不存在的字段
}
}
}
}
}
/**
* 生成拦截器的代理对象
*
* @param target 目标对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置属性
* 可以在这里设置一些配置项
*
* @param properties 属性配置
*/
@Override
public void setProperties(Properties properties) {
// 可以在这里设置一些配置项
}
}
SQL 注入安全保护
MyBatis-Plus 提供了自动和手动两种方式来检查 SQL 注入风险。
自动检查
使用 Wrappers.query()
方法时,可以通过 .checkSqlInjection()
开启自动检查。
Wrappers.query() // 开启自动检查 SQL 注入 .checkSqlInjection().orderByDesc("任意前端传入字段,我们推荐最好是白名单处理,因为可能存在检查覆盖不全情况")
手动校验
使用 SqlInjectionUtils.check()
方法进行手动校验。
// 手动校验前端传入的字段是否存在 SQL 注入风险 SqlInjectionUtils.check("任意前端传入字段,我们推荐最好是白名单处理,因为可能存在检查覆盖不全情况")
/**
* SQL 注入验证工具类
*
* 提供SQL注入检查和处理相关的方法,以帮助提高应用程序的安全性
*/
public class SqlInjectionUtils {
/**
* SQL语法检查正则:符合两个关键字(有先后顺序)才算匹配
*/
private static final Pattern SQL_SYNTAX_PATTERN = Pattern.compile("(insert|delete|update|select|create|drop|truncate|grant|alter|deny|revoke|call|execute|exec|declare|show|rename|set)" +
"\\s+.*(into|from|set|where|table|database|view|index|on|cursor|procedure|trigger|for|password|union|and|or)|(select\\s*\\*\\s*from\\s+)" +
"|if\\s*\\(.*\\)|select\\s*\\(.*\\)|substr\\s*\\(.*\\)|substring\\s*\\(.*\\)|char\\s*\\(.*\\)|concat\\s*\\(.*\\)|benchmark\\s*\\(.*\\)|sleep\\s*\\(.*\\)|(and|or)\\s+.*", Pattern.CASE_INSENSITIVE);
/**
* 使用'、;或注释截断SQL检查正则
*/
private static final Pattern SQL_COMMENT_PATTERN = Pattern.compile("'.*(or|union|--|#|/\\*|;)", Pattern.CASE_INSENSITIVE);
/**
* 检查参数是否存在 SQL 注入
*
* @param value 检查参数
* @return true 非法 false 合法
*/
public static boolean check(String value) {
Objects.requireNonNull(value);
// 处理是否包含SQL注释字符 || 检查是否包含SQL注入敏感字符
return SQL_COMMENT_PATTERN.matcher(value).find() || SQL_SYNTAX_PATTERN.matcher(value).find();
}
/**
* 刪除字段转义符单引号双引号
*
* @param text 待处理字段
*/
public static String removeEscapeCharacter(String text) {
Objects.nonNull(text);
return text.replaceAll("\"", "").replaceAll("'", "");
}
}
最好的预防方式仍旧是不允许任何SQL片段由前端传到后台,我们强烈建议不要开放给前端太多的动态 SQL,这样最安全。