MyBatis-Plus 学习笔记-数据安全保护

        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,这样最安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕德猫宁丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值