diff --git a/pom.xml b/pom.xml index 5efd58c9a..a897d31d9 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 2.14.2 2.3.1 1.18.24 + 1.72 1.33 @@ -264,6 +265,13 @@ ${snakeyaml.version} + + + org.bouncycastle + bcprov-jdk15to18 + ${bouncycastle.version} + + com.ruoyi diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 0468a33ac..6c1fa63e2 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -179,6 +179,19 @@ mybatis-plus: updateStrategy: NOT_NULL # 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件 where-strategy: NOT_NULL +# 数据加密 +mybatis-encryptor: + # 是否开启加密 + enabled: false + # 默认加密算法 + algorithm: base64 + # 安全秘钥。对称算法的秘钥。如:AES,SM4 +# password: QBcc3SHK2G4ijyfl5XQ7@2g!N2j2jRDR +# # 公私钥。非对称算法的公私钥,如:SM2,RSA +# publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDt41GIcWvY6/tpjOLoWY78Oy00uWcNbMRG/8DRoS79/h2D+pV8uxV+0ezaN+fBFCZnK8TdJcPeU4EnNRUh/8HEY33KFvZ700n+Gj5BHUUDKzx3UVFNuF49UI/yoJ8rz6VQQHO79KK89VwmSMO77Tfee1ofe0STY6IwMt/MwaoKwIDAQAB +# privateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIO3jUYhxa9jr+2mM4uhZjvw7LTS5Zw1sxEb/wNGhLv3+HYP6lXy7FX7R7No358EUJmcrxN0lw95TgSc1FSH/wcRjfcoW9nvTSf4aPkEdRQMrPHdRUU24Xj1Qj/KgnyvPpVBAc7v0orz1XCZIw7vtN957Wh97RJNjojAy38zBqgrAgMBAAECgYAkXx4qvI6rFNryw88+Am6JpMioUghHb2ioE9QCYomqohnA+DocS71JLN8qwo3lijp7gJGzzKEeC8Aoc+oKAZfBQQwPP0K/ql+NcLdlqNAr+XyzkZrLJD/BfluQ6mXI23tjonWnPPZ1Et9HC3zXRQPiuh7Ff6UoqatTMI3OIbSyuQJBAPbaJpVYmIsWJdpvaAC6hcGgR/vbG9yX8VphPn8/2wsm2MmQrRkUoKkudE/veX3KQjGikDwo9+bT6aBl+nn2DUUCQQCImSGZFNcGqhMLyRGVogP6fh0YZSEQAhFbZ69nQmplqbie+dREhODH5Vs5F0C2aL5iSR/UaIDkNEA1auX7x56vAkAFWjON927PTTqi4tmBconl6eDFsDmJbe34xLUDM1I/iqcWr8FhEtZs9Knm9c1PkewfgWPZOhYt9hhRtwRYUqJ1AkAPvQ0I9US9KNVe80DKa8tnjiZODEDd9k8HqA+mpxlZM0/pSUGyz1iSz5NOJaa4HaNp8aDwOUY4hOis/u8Wrm5TAkEA00YeUsaXlMyMF/5pjols44tXb54AjAC2mH66pz9JsKg7pKpWVOpEV5rMY58CGZHWau69vGLZnCd1coeMw77YAQ== +# # 编码方式,base64/hex。默认base64 +# encode: base64 # Swagger配置 swagger: diff --git a/ruoyi-admin/src/test/java/com/ruoyi/test/EncryptUnitTest.java b/ruoyi-admin/src/test/java/com/ruoyi/test/EncryptUnitTest.java new file mode 100644 index 000000000..b049232ab --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/test/EncryptUnitTest.java @@ -0,0 +1,37 @@ +package com.ruoyi.test; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.ruoyi.demo.domain.TestDemo; +import com.ruoyi.demo.mapper.TestDemoMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; + +/** + * 加密单元测试案例 + * + * @author 老马 + * @date 2023-01-12 08:53 + */ +@SpringBootTest +@SaIgnore +@DisplayName("加密测试") +public class EncryptUnitTest { + + @Resource + private TestDemoMapper demoMapper; + + @Test + public void testCrypt() { + TestDemo demo = new TestDemo(); + demo.setTestKey("测试的key"); + demo.setValue("测试的value"); + this.demoMapper.insert(demo); + System.out.println(demo); + TestDemo testDemo = this.demoMapper.selectById(demo.getId()); + System.out.println(testDemo); + } + +} diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index dfeead2a2..f302d3e7b 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -153,6 +153,12 @@ lock4j-redisson-spring-boot-starter + + + org.bouncycastle + bcprov-jdk15to18 + + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java new file mode 100644 index 000000000..d44eb293c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java @@ -0,0 +1,44 @@ +package com.ruoyi.common.annotation; + +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; + +import java.lang.annotation.*; + +/** + * 字段加密注解 + * + * @author 老马 + */ +@Documented +@Inherited +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EncryptField { + + /** + * 加密算法 + */ + AlgorithmType algorithm() default AlgorithmType.BASE64; + + /** + * 秘钥。AES、SM4需要 + */ + String password() default ""; + + /** + * 公钥。RSA、SM2需要 + */ + String publicKey() default ""; + + /** + * 公钥。RSA、SM2需要 + */ + String privateKey() default ""; + + /** + * 编码方式。对加密算法为BASE64的不起作用 + */ + EncodeType encode() default EncodeType.BASE64; + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/EncryptContext.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/EncryptContext.java new file mode 100644 index 000000000..3340ecdc1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/EncryptContext.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.encrypt; + +import com.ruoyi.common.enums.EncodeType; +import lombok.Builder; +import lombok.Data; + +/** + * 加密上下文。用于encryptor传递必要的参数。 + * 隔离配置和注解 + * + * @author 老马 + * @date 2023-01-17 08:31 + */ +@Data +public class EncryptContext { + /** + * 安全秘钥 + */ + private String password; + + /** + * 公钥 + */ + private String publicKey; + + /** + * 私钥 + */ + private String privateKey; + + /** + * 编码方式,base64/hex + */ + private EncodeType encode; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/IEncryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/IEncryptor.java new file mode 100644 index 000000000..fae0327c1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/IEncryptor.java @@ -0,0 +1,46 @@ +package com.ruoyi.common.encrypt; + +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; + +/** + * 加解者 + * + * @author 老马 + * @date 2023-01-10 16:08 + */ +public interface IEncryptor { + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + AlgorithmType algorithm(); + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String 加密后的字符串 + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + String encrypt(String value, EncodeType encodeType) throws Exception; + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String 解密后的字符串 + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + String decrypt(String value, EncodeType encodeType) throws Exception; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AbstractEncryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AbstractEncryptor.java new file mode 100644 index 000000000..5e7ce73d0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AbstractEncryptor.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.encrypt.encryptor; + +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.encrypt.IEncryptor; + +/** + * 所有加密执行者的基类 + * + * @author 老马 + * @date 2023-01-17 16:52 + */ +public abstract class AbstractEncryptor implements IEncryptor { + public AbstractEncryptor(EncryptContext context) { + //子类必须实现带参数的构造方法 + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AesEncryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AesEncryptor.java new file mode 100644 index 000000000..f0d3a3069 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/AesEncryptor.java @@ -0,0 +1,87 @@ +package com.ruoyi.common.encrypt.encryptor; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; + +import java.nio.charset.StandardCharsets; + +/** + * AES算法实现 + * + * @author 老马 + * @date 2023-01-06 11:39 + */ +public class AesEncryptor extends AbstractEncryptor { + + private AES aes = null; + + public AesEncryptor(EncryptContext context) { + super(context); + String password = context.getPassword(); + if (StrUtil.isBlank(password)) { + throw new RuntimeException("aes没有获得秘钥信息"); + } + // aes算法的秘钥要求是16位、24位、32位 + int[] array = {16, 24, 32}; + if(!ArrayUtil.contains(array, password.length())) { + throw new RuntimeException("aes秘钥长度应该为16位、24位、32位,实际为"+password.length()+"位"); + } + aes = SecureUtil.aes(context.getPassword().getBytes(StandardCharsets.UTF_8)); + } + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + @Override + public AlgorithmType algorithm() { + return AlgorithmType.AES; + } + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String encrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.aes)) { + if (encodeType == EncodeType.HEX) { + return aes.encryptHex(value); + } else { + return aes.encryptBase64(value); + } + } + return value; + } + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String decrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.aes)) { + return this.aes.decryptStr(value); + } + return value; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Base64Encryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Base64Encryptor.java new file mode 100644 index 000000000..bd5038e4b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Base64Encryptor.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.encrypt.encryptor; + +import cn.hutool.core.codec.Base64; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; + +/** + * Base64算法实现。不建议在生产环境使用 + * + * @author 老马 + * @date 2023-01-06 10:00 + */ +public class Base64Encryptor extends AbstractEncryptor { + + public Base64Encryptor(EncryptContext context) { + super(context); + } + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + @Override + public AlgorithmType algorithm() { + return AlgorithmType.BASE64; + } + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String encrypt(String value, EncodeType encodeType) throws Exception { + return Base64.encode(value); + } + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String decrypt(String value, EncodeType encodeType) throws Exception { + return Base64.decodeStr(value); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/RsaEncryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/RsaEncryptor.java new file mode 100644 index 000000000..40d767c06 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/RsaEncryptor.java @@ -0,0 +1,83 @@ +package com.ruoyi.common.encrypt.encryptor; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; +import com.ruoyi.common.utils.StringUtils; + + +/** + * RSA算法实现 + * + * @author 老马 + * @date 2023-01-06 09:37 + */ +public class RsaEncryptor extends AbstractEncryptor { + + private RSA rsa = null; + + public RsaEncryptor(EncryptContext context) { + super(context); + String privateKey = context.getPrivateKey(); + String publicKey = context.getPublicKey(); + if (StringUtils.isAnyEmpty(privateKey, publicKey)) { + throw new RuntimeException("rsa公私钥均需要提供,公钥加密,私钥解密。"); + } + this.rsa = SecureUtil.rsa(Base64.decode(privateKey), Base64.decode(publicKey)); + } + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + @Override + public AlgorithmType algorithm() { + return AlgorithmType.RSA; + } + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String encrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.rsa)) { + if (encodeType == EncodeType.HEX) { + return rsa.encryptHex(value, KeyType.PublicKey); + } else { + return rsa.encryptBase64(value, KeyType.PublicKey); + } + } + return value; + } + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String decrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.rsa)) { + return this.rsa.decryptStr(value, KeyType.PrivateKey); + } + return value; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm2Encryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm2Encryptor.java new file mode 100644 index 000000000..71f0620ca --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm2Encryptor.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.encrypt.encryptor; + + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.SM2; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; +import com.ruoyi.common.utils.StringUtils; + +/** + * sm2算法实现 + * + * @author 老马 + * @date 2023-01-06 17:13 + */ +public class Sm2Encryptor extends AbstractEncryptor { + + private SM2 sm2 = null; + + public Sm2Encryptor(EncryptContext context) { + super(context); + String privateKey = context.getPrivateKey(); + String publicKey = context.getPublicKey(); + if (StringUtils.isAnyEmpty(privateKey, publicKey)) { + throw new RuntimeException("sm2公私钥均需要提供,公钥加密,私钥解密。"); + } + this.sm2 = SmUtil.sm2(Base64.decode(privateKey), Base64.decode(publicKey)); + } + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + @Override + public AlgorithmType algorithm() { + return AlgorithmType.SM2; + } + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String encrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.sm2)) { + if (encodeType == EncodeType.HEX) { + return sm2.encryptHex(value, KeyType.PublicKey); + } else { + return sm2.encryptBase64(value, KeyType.PublicKey); + } + } + return value; + } + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String decrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.sm2)) { + return this.sm2.decryptStr(value, KeyType.PrivateKey); + } + return value; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm4Encryptor.java b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm4Encryptor.java new file mode 100644 index 000000000..f30afdce6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/encrypt/encryptor/Sm4Encryptor.java @@ -0,0 +1,87 @@ +package com.ruoyi.common.encrypt.encryptor; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.symmetric.SM4; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; + +import java.nio.charset.StandardCharsets; + +/** + * sm4算法实现 + * + * @author 老马 + * @date 2023-01-06 17:40 + */ +public class Sm4Encryptor extends AbstractEncryptor { + + private SM4 sm4 = null; + + public Sm4Encryptor(EncryptContext context) { + super(context); + String password = context.getPassword(); + if (StrUtil.isBlank(password)) { + throw new RuntimeException("sm4没有获得秘钥信息"); + } + // sm4算法的秘钥要求是16位长度 + if (16 != password.length()) { + throw new RuntimeException("sm4秘钥长度应该为16位,实际为" + password.length() + "位"); + } + this.sm4 = SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 获得当前算法 + * + * @return com.ruoyi.common.enums.AlgorithmType + * @author 老马 + * @date 2023/1/11 11:18 + */ + @Override + public AlgorithmType algorithm() { + return AlgorithmType.SM4; + } + + /** + * 加密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String encrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.sm4)) { + if (encodeType == EncodeType.HEX) { + return sm4.encryptHex(value); + } else { + return sm4.encryptBase64(value); + } + } + return value; + } + + /** + * 解密 + * + * @param value 待加密字符串 + * @param encodeType 加密后的编码格式 + * @return java.lang.String + * @throws Exception 抛出异常 + * @author 老马 + * @date 2023/1/10 16:38 + */ + @Override + public String decrypt(String value, EncodeType encodeType) throws Exception { + if (ObjectUtil.isNotNull(this.sm4)) { + return this.sm4.decryptStr(value); + } + return value; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/AlgorithmType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/AlgorithmType.java new file mode 100644 index 000000000..eb9b830a0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/AlgorithmType.java @@ -0,0 +1,42 @@ +package com.ruoyi.common.enums; + +import com.ruoyi.common.encrypt.IEncryptor; +import com.ruoyi.common.encrypt.encryptor.*; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 算法名称 + * + * @author 老马 + */ +@Getter +@AllArgsConstructor +public enum AlgorithmType { + /** + * base64 + */ + BASE64(Base64Encryptor.class), + + /** + * aes + */ + AES(AesEncryptor.class), + + /** + * rsa + */ + RSA(RsaEncryptor.class), + + /** + * sm2 + */ + SM2(Sm2Encryptor.class), + + /** + * sm4 + */ + SM4(Sm4Encryptor.class); + + private final Class clazz; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/EncodeType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/EncodeType.java new file mode 100644 index 000000000..24cdab63a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/EncodeType.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 编码类型 + * + * @author 老马 + * @date 2023-01-11 11:39 + */ +public enum EncodeType { + /** + * base64编码 + */ + BASE64, + + /** + * 16进制编码 + */ + HEX; + +} diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/TestDemo.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/TestDemo.java index dc19ce2c7..e32843a44 100644 --- a/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/TestDemo.java +++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/TestDemo.java @@ -1,7 +1,9 @@ package com.ruoyi.demo.domain; import com.baomidou.mybatisplus.annotation.*; +import com.ruoyi.common.annotation.EncryptField; import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.enums.AlgorithmType; import lombok.Data; import lombok.EqualsAndHashCode; @@ -44,11 +46,15 @@ public class TestDemo extends BaseEntity { /** * key键 */ +// @EncryptField(algorithm=AlgorithmType.SM2, privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgZSlOvw8FBiH+aFJWLYZP/VRjg9wjfRarTkGBZd/T3N+gCgYIKoEcz1UBgi2hRANCAAR5DGuQwJqkxnbCsP+iPSDoHWIF4RwcR5EsSvT8QPxO1wRkR2IhCkzvRb32x2CUgJFdvoqVqfApFDPZzShqzBwX", publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEeQxrkMCapMZ2wrD/oj0g6B1iBeEcHEeRLEr0/ED8TtcEZEdiIQpM70W99sdglICRXb6KlanwKRQz2c0oaswcFw==") + @EncryptField(algorithm = AlgorithmType.RSA, privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANBBEeueWlXlkkj2+WY5l+IWe42d8b5K28g+G/CFKC/yYAEHtqGlCsBOrb+YBkG9mPzmuYA/n9k0NFIc8E8yY5vZQaroyFBrTTWEzG9RY2f7Y3svVyybs6jpXSUs4xff8abo7wL1Y/wUaeatTViamxYnyTvdTmLm3d+JjRij68rxAgMBAAECgYAB0TnhXraSopwIVRfmboea1b0upl+BUdTJcmci412UjrKr5aE695ZLPkXbFXijVu7HJlyyv94NVUdaMACV7Ku/S2RuNB70M7YJm8rAjHFC3/i2ZeIM60h1Ziy4QKv0XM3pRATlDCDNhC1WUrtQCQSgU8kcp6eUUppruOqDzcY04QJBAPm9+sBP9CwDRgy3e5+V8aZtJkwDstb0lVVV/KY890cydVxiCwvX3fqVnxKMlb+x0YtH0sb9v+71xvK2lGobaRECQQDVePU6r/cCEfpc+nkWF6osAH1f8Mux3rYv2DoBGvaPzV2BGfsLed4neRfCwWNCKvGPCdW+L0xMJg8+RwaoBUPhAkAT5kViqXxFPYWJYd1h2+rDXhMdH3ZSlm6HvDBDdrwlWinr0Iwcx3iSjPV93uHXwm118aUj4fg3LDJMCKxOwBxhAkByrQXfvwOMYygBprRBf/j0plazoWFrbd6lGR0f1uI5IfNnFRPdeFw1DEINZ2Hw+6zEUF44SqRMC+4IYJNc02dBAkBCgy7RvfyV/A7N6kKXxTHauY0v6XwSSvpeKtRJkbIcRWOdIYvaHO9L7cklj3vIEdwjSUp9K4VTBYYlmAz1xh03", publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQQRHrnlpV5ZJI9vlmOZfiFnuNnfG+StvIPhvwhSgv8mABB7ahpQrATq2/mAZBvZj85rmAP5/ZNDRSHPBPMmOb2UGq6MhQa001hMxvUWNn+2N7L1csm7Oo6V0lLOMX3/Gm6O8C9WP8FGnmrU1YmpsWJ8k73U5i5t3fiY0Yo+vK8QIDAQAB") private String testKey; /** * 值 */ + //@EncryptField(algorithm = AlgorithmType.SM4, password = "10rfylhtccpuyke5") + @EncryptField(algorithm = AlgorithmType.AES, password = "10rfylhtccpuyke5") private String value; /** diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/EncryptorConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/EncryptorConfig.java new file mode 100644 index 000000000..7d5d21a76 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/EncryptorConfig.java @@ -0,0 +1,37 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.framework.config.properties.EncryptorProperties; +import com.ruoyi.framework.encrypt.EncryptorManager; +import com.ruoyi.framework.encrypt.MybatisDecryptInterceptor; +import com.ruoyi.framework.encrypt.MybatisEncryptInterceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 加解密配置 + * + * @author 老马 + * @date 2023-01-11 10:03 + */ +@Configuration +@ConditionalOnProperty(value = "mybatis-encryptor.enabled", havingValue = "true") +public class EncryptorConfig { + + @Bean + public EncryptorManager mybatisCryptHandler(EncryptorProperties properties) { + EncryptorManager encryptorManager = new EncryptorManager(); + encryptorManager.registAndGetEncryptor(properties); + return encryptorManager; + } + + @Bean + public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorProperties properties) { + return new MybatisEncryptInterceptor(); + } + + @Bean + public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorProperties properties) { + return new MybatisDecryptInterceptor(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/EncryptorProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/EncryptorProperties.java new file mode 100644 index 000000000..d34949d37 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/EncryptorProperties.java @@ -0,0 +1,49 @@ +package com.ruoyi.framework.config.properties; + +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 加解密属性配置类 + * + * @author 老马 + * @date 2023-01-10 16:52 + */ +@Data +@Component +@ConfigurationProperties(prefix = "mybatis-encryptor") +public class EncryptorProperties { + + /** + * 过滤开关 + */ + private Boolean enabled; + + /** + * 默认算法 + */ + private AlgorithmType algorithm; + + /** + * 安全秘钥 + */ + private String password; + + /** + * 公钥 + */ + private String publicKey; + + /** + * 私钥 + */ + private String privateKey; + + /** + * 编码方式,base64/hex + */ + private EncodeType encode; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptedFieldsCache.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptedFieldsCache.java new file mode 100644 index 000000000..e9d2a68ab --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptedFieldsCache.java @@ -0,0 +1,33 @@ +package com.ruoyi.framework.encrypt; + +import com.ruoyi.common.annotation.EncryptField; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 类加密字段缓存类 + * + * @author 老马 + * @date 2023-01-12 11:07 + */ +public class EncryptedFieldsCache { + private static final Map, Set> ENCRYPTED_FIELD_CACHE = new ConcurrentHashMap<>(); + + public static Set get(Class sourceClazz) { + return ENCRYPTED_FIELD_CACHE.computeIfAbsent(sourceClazz, clazz -> { + Field[] declaredFields = clazz.getDeclaredFields(); + Set fieldSet = Arrays.stream(declaredFields).filter(field -> + field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class) + .collect(Collectors.toSet()); + for (Field field : fieldSet) { + field.setAccessible(true); + } + return fieldSet; + }); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptorManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptorManager.java new file mode 100644 index 000000000..d929cc2cd --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/EncryptorManager.java @@ -0,0 +1,120 @@ +package com.ruoyi.framework.encrypt; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.encrypt.IEncryptor; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.config.properties.EncryptorProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 加密管理类 + * + * @author 老马 + * @date 2023-01-11 10:07 + */ +@Slf4j +public class EncryptorManager { + + /** + * 缓存加密器 + */ + Map encryptorMap = new ConcurrentHashMap<>(); + + + /** + * 注册加密执行者到缓存 + * + * @param properties 加密执行者需要的相关配置参数 + * @author 老马 + * @date 2023/1/11 15:09 + */ + public IEncryptor registAndGetEncryptor(EncryptorProperties properties) { + String encryptorKey = this.getEncryptorKeyFromProperties(properties); + if (encryptorMap.containsKey(encryptorKey)) { + return encryptorMap.get(encryptorKey); + } + EncryptContext encryptContext = new EncryptContext(); + encryptContext.setPassword(properties.getPassword()); + encryptContext.setPrivateKey(properties.getPrivateKey()); + encryptContext.setPublicKey(properties.getPublicKey()); + encryptContext.setEncode(properties.getEncode()); + IEncryptor encryptor = ReflectUtil.newInstance(properties.getAlgorithm().getClazz(), encryptContext); + encryptorMap.put(encryptorKey, encryptor); + return encryptorMap.get(encryptorKey); + } + + /** + * 移除缓存中的加密执行者 + * + * @param properties 加密执行者需要的相关配置参数 + * @author 老马 + * @date 2023/1/11 15:55 + */ + public void removeEncryptor(EncryptorProperties properties) { + this.encryptorMap.remove(getEncryptorKeyFromProperties(properties)); + } + + /** + * 根据配置进行加密。会进行本地缓存对应的算法和对应的秘钥信息。 + * + * @param value 待加密的值 + * @param properties 加密相关的配置信息 + * @return java.lang.String 加密后的结果 + * @author 老马 + * @date 2023/1/11 16:46 + */ + public String encrypt(String value, EncryptorProperties properties) { + try { + IEncryptor encryptor = this.registAndGetEncryptor(properties); + if(ObjectUtil.isNull(encryptor)){ + return value; + } + return encryptor.encrypt(value, properties.getEncode()); + } catch (Exception e) { + log.error("字段加密异常,原样返回", e); + return value; + } + } + + /** + * 根据配置进行解密 + * + * @param value 待解密的值 + * @param properties 加密相关的配置信息 + * @return java.lang.String + * @author 老马 + * @date 2023/1/11 17:43 + */ + public String decrypt(String value, EncryptorProperties properties) { + try { + IEncryptor encryptor = this.registAndGetEncryptor(properties); + if(ObjectUtil.isNull(encryptor)){ + return value; + } + return encryptor.decrypt(value, properties.getEncode()); + } catch (Exception e) { + log.error("字段解密异常,原样返回", e); + return value; + } + } + + + /** + * 从配置内容中提取缓存的KEY + * + * @param properties 加密相关的配置信息 + * @return java.lang.String + * @author 老马 + * @date 2023/1/11 17:39 + */ + private String getEncryptorKeyFromProperties(EncryptorProperties properties) { + return properties.getAlgorithm() + StringUtils.defaultString(properties.getPassword()) + + StringUtils.defaultString(properties.getPublicKey()) + StringUtils.defaultString(properties.getPrivateKey()); + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisDecryptInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisDecryptInterceptor.java new file mode 100644 index 000000000..1cdb58e1f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisDecryptInterceptor.java @@ -0,0 +1,105 @@ +package com.ruoyi.framework.encrypt; + +import cn.hutool.core.collection.CollectionUtil; +import com.ruoyi.common.annotation.EncryptField; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.EncryptorProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.resultset.ResultSetHandler; +import org.apache.ibatis.plugin.*; + +import java.lang.reflect.Field; +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * 出参解密拦截器 + * + * @author 老马 + * @date 2023-01-12 13:47 + */ +@Slf4j +@Intercepts({@Signature( + type = ResultSetHandler.class, + method = "handleResultSets", + args = {Statement.class}) +}) +public class MybatisDecryptInterceptor implements Interceptor { + + private final EncryptorManager encryptorManager = SpringUtils.getBean(EncryptorManager.class); + private final EncryptorProperties defaultProperties = SpringUtils.getBean(EncryptorProperties.class); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + // 获取执行mysql执行结果 + Object result = invocation.proceed(); + if (result == null) { + return null; + } + decryptHandler(result); + return result; + } + + /** + * 解密对象 + * + * @param sourceObject 待加密对象 + */ + private void decryptHandler(Object sourceObject) { + if (sourceObject instanceof Map) { + ((Map) sourceObject).values().forEach(this::decryptHandler); + return; + } + if (sourceObject instanceof List) { + // 判断第一个元素是否含有注解。如果没有直接返回,提高效率 + Object firstItem = ((List) sourceObject).get(0); + if (CollectionUtil.isEmpty(EncryptedFieldsCache.get(firstItem.getClass()))) { + return; + } + ((List) sourceObject).forEach(this::decryptHandler); + return; + } + Set fields = EncryptedFieldsCache.get(sourceObject.getClass()); + try { + for (Field field : fields) { + field.set(sourceObject, this.decryptField(String.valueOf(field.get(sourceObject)), field)); + } + } catch (Exception e) { + log.error("处理解密字段时出错", e); + } + } + + /** + * 字段值进行加密。通过字段的批注注册新的加密算法 + * + * @param value 待加密的值 + * @param field 待加密字段 + * @return 加密后结果 + */ + private String decryptField(String value, Field field) { + EncryptField encryptField = field.getAnnotation(EncryptField.class); + EncryptorProperties properties = new EncryptorProperties(); + properties.setEnabled(true); + properties.setAlgorithm(encryptField.algorithm()); + properties.setEncode(encryptField.encode()); + properties.setPassword(StringUtils.isEmpty(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password()); + properties.setPrivateKey(StringUtils.isEmpty(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey()); + properties.setPublicKey(StringUtils.isEmpty(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey()); + this.encryptorManager.registAndGetEncryptor(properties); + return this.encryptorManager.decrypt(value, properties); + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisEncryptInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisEncryptInterceptor.java new file mode 100644 index 000000000..4fd5289e0 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisEncryptInterceptor.java @@ -0,0 +1,111 @@ +package com.ruoyi.framework.encrypt; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.ruoyi.common.annotation.EncryptField; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.EncryptorProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Signature; + +import java.lang.reflect.Field; +import java.sql.PreparedStatement; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * 入参加密拦截器 + * + * @author 老马 + * @date 2023-01-10 16:42 + */ +@Slf4j +@Intercepts({@Signature( + type = ParameterHandler.class, + method = "setParameters", + args = {PreparedStatement.class}) +}) +public class MybatisEncryptInterceptor implements Interceptor { + + private final EncryptorManager encryptorManager = SpringUtils.getBean(EncryptorManager.class); + private final EncryptorProperties defaultProperties = SpringUtils.getBean(EncryptorProperties.class); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + return invocation; + } + + @Override + public Object plugin(Object target) { + if (target instanceof ParameterHandler) { + // 进行加密操作 + ParameterHandler parameterHandler = (ParameterHandler) target; + Object parameterObject = parameterHandler.getParameterObject(); + if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) { + this.encryptHandler(parameterObject); + } + } + return target; + } + + /** + * 加密对象 + * + * @param sourceObject 待加密对象 + */ + private void encryptHandler(Object sourceObject) { + if (sourceObject instanceof Map) { + ((Map) sourceObject).values().forEach(this::encryptHandler); + return; + } + if (sourceObject instanceof List) { + // 判断第一个元素是否含有注解。如果没有直接返回,提高效率 + Object firstItem = ((List) sourceObject).get(0); + if (CollectionUtil.isEmpty(EncryptedFieldsCache.get(firstItem.getClass()))) { + return; + } + ((List) sourceObject).forEach(this::encryptHandler); + return; + } + Set fields = EncryptedFieldsCache.get(sourceObject.getClass()); + try { + for (Field field : fields) { + field.set(sourceObject, this.encryptField(String.valueOf(field.get(sourceObject)), field)); + } + } catch (Exception e) { + log.error("处理加密字段时出错", e); + } + } + + /** + * 字段值进行加密。通过字段的批注注册新的加密算法 + * + * @param value 待加密的值 + * @param field 待加密字段 + * @return 加密后结果 + */ + private String encryptField(String value, Field field) { + EncryptField encryptField = field.getAnnotation(EncryptField.class); + EncryptorProperties properties = new EncryptorProperties(); + properties.setEnabled(true); + properties.setAlgorithm(encryptField.algorithm()); + properties.setEncode(encryptField.encode()); + properties.setPassword(StringUtils.isEmpty(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password()); + properties.setPrivateKey(StringUtils.isEmpty(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey()); + properties.setPublicKey(StringUtils.isEmpty(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey()); + this.encryptorManager.registAndGetEncryptor(properties); + return this.encryptorManager.encrypt(value, properties); + } + + + @Override + public void setProperties(Properties properties) { + } +}