diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index a6617bc6f..8b9ba89ef 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -196,6 +196,16 @@ mybatis-encryptor: publicKey: privateKey: +# 数据加密 +request-encryptor: + # 是否开启加密 + enable: true + # AES 加密头标识 + headerFlag: AES + # 公私钥 非对称算法的公私钥 如:SM2,RSA + publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ== + privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y= + # Swagger配置 swagger: info: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/CryptoFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/CryptoFilter.java new file mode 100644 index 000000000..ae9f749fd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/CryptoFilter.java @@ -0,0 +1,60 @@ +package com.ruoyi.common.filter; + +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.encrypt.encryptor.RsaEncryptor; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.http.MediaType; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Crypto 过滤器 + * + * @author wdhcr + */ +public class CryptoFilter implements Filter { + + public static final String CRYPTO_ENABLE = "enable"; + public static final String CRYPTO_PUBLIC_KEY = "publicKey"; + public static final String CRYPTO_PRIVATE_KEY = "privateKey"; + public static final String CRYPTO_HEADER_FLAG = "headerFlag"; + private RsaEncryptor rsaEncryptor; + private boolean enable; + private String headerFlag; + + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String enableStr = filterConfig.getInitParameter(CryptoFilter.CRYPTO_ENABLE); + enable = Boolean.parseBoolean(enableStr); + EncryptContext encryptContext = new EncryptContext(); + encryptContext.setAlgorithm(AlgorithmType.RSA); + encryptContext.setPublicKey(filterConfig.getInitParameter(CryptoFilter.CRYPTO_PUBLIC_KEY)); + encryptContext.setPrivateKey(filterConfig.getInitParameter(CryptoFilter.CRYPTO_PRIVATE_KEY)); + headerFlag = filterConfig.getInitParameter(CryptoFilter.CRYPTO_HEADER_FLAG); + rsaEncryptor = new RsaEncryptor(encryptContext); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + ServletRequest requestWrapper = null; + if (enable && request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { + requestWrapper = new DecryptRequestBodyWrapper((HttpServletRequest) request, rsaEncryptor, headerFlag); + } + if (null == requestWrapper) { + chain.doFilter(request, response); + } else { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() { + + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/DecryptRequestBodyWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/DecryptRequestBodyWrapper.java new file mode 100644 index 000000000..5bb653b72 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/DecryptRequestBodyWrapper.java @@ -0,0 +1,106 @@ +package com.ruoyi.common.filter; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IoUtil; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.encrypt.EncryptContext; +import com.ruoyi.common.encrypt.encryptor.AesEncryptor; +import com.ruoyi.common.encrypt.encryptor.RsaEncryptor; +import com.ruoyi.common.enums.AlgorithmType; +import com.ruoyi.common.enums.EncodeType; +import com.ruoyi.common.exception.base.BaseException; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 解密请求参数工具类 + * + * @author wdhcr + */ +public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public DecryptRequestBodyWrapper(HttpServletRequest request, RsaEncryptor rsaEncryptor, String headerFlag) throws IOException { + super(request); + String requestRsa = request.getHeader(headerFlag); + if (StringUtils.isEmpty(requestRsa)) { + throw new BaseException("加密AES的动态密码不能为空"); + } + String decryptAes = new String(Base64.decode(rsaEncryptor.decrypt(requestRsa))); + request.setCharacterEncoding(Constants.UTF8); + byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false); + String requestBody = StringUtils.toEncodedString(readBytes, StandardCharsets.UTF_8); + EncryptContext encryptContext = new EncryptContext(); + encryptContext.setAlgorithm(AlgorithmType.AES); + encryptContext.setPassword(decryptAes); + encryptContext.setEncode(EncodeType.BASE64); + AesEncryptor aesEncryptor = new AesEncryptor(encryptContext); + String decryptBody = aesEncryptor.decrypt(requestBody); + body = decryptBody.getBytes(StandardCharsets.UTF_8); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public String getContentType() { + return MediaType.APPLICATION_JSON_VALUE; + } + + + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public int available() { + return body.length; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java index 0770c2162..7738b8054 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java @@ -1,8 +1,10 @@ package com.ruoyi.framework.config; +import com.ruoyi.common.filter.CryptoFilter; import com.ruoyi.common.filter.RepeatableFilter; import com.ruoyi.common.filter.XssFilter; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.config.properties.RequestEncryptProperties; import com.ruoyi.framework.config.properties.XssProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -25,6 +27,27 @@ public class FilterConfig { @Autowired private XssProperties xssProperties; + @Autowired + private RequestEncryptProperties requestEncryptProperties; + + @Bean + @ConditionalOnProperty(value = "request-encryptor.enable", havingValue = "true") + public FilterRegistrationBean cryptoFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new CryptoFilter()); + registration.addUrlPatterns("/*"); + registration.setName("cryptoFilter"); + HashMap param = new HashMap<>(); + param.put(CryptoFilter.CRYPTO_ENABLE, String.valueOf(requestEncryptProperties.getEnable())); + param.put(CryptoFilter.CRYPTO_PUBLIC_KEY, requestEncryptProperties.getPublicKey()); + param.put(CryptoFilter.CRYPTO_PRIVATE_KEY, requestEncryptProperties.getPrivateKey()); + param.put(CryptoFilter.CRYPTO_HEADER_FLAG, requestEncryptProperties.getHeaderFlag()); + registration.setInitParameters(param); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + return registration; + } + @SuppressWarnings({"rawtypes", "unchecked"}) @Bean @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/RequestEncryptProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/RequestEncryptProperties.java new file mode 100644 index 000000000..05b6ce390 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/RequestEncryptProperties.java @@ -0,0 +1,36 @@ +package com.ruoyi.framework.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 请求加解密属性配置类 + * @author wdhcr + */ +@Data +@Component +@ConfigurationProperties(prefix = "request-encryptor") +public class RequestEncryptProperties { + + /** + * 加密开关 + */ + private Boolean enable; + + /** + * 头部标识 + */ + private String headerFlag; + + + /** + * 公钥 + */ + private String publicKey; + + /** + * 私钥 + */ + private String privateKey; +} diff --git a/ruoyi-ui-vue3/.env.development b/ruoyi-ui-vue3/.env.development index ac4d2c13f..64e05213a 100644 --- a/ruoyi-ui-vue3/.env.development +++ b/ruoyi-ui-vue3/.env.development @@ -10,6 +10,9 @@ VITE_APP_BASE_API = '/dev-api' # 应用访问路径 例如使用前缀 /admin/ VITE_APP_CONTEXT_PATH = '/' +# 请求接口是否加密 +IS_ENCRYPT = 'true' + # 监控地址 VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications' diff --git a/ruoyi-ui-vue3/.env.production b/ruoyi-ui-vue3/.env.production index 18bb273cb..ebdc80f40 100644 --- a/ruoyi-ui-vue3/.env.production +++ b/ruoyi-ui-vue3/.env.production @@ -7,6 +7,9 @@ VITE_APP_ENV = 'production' # 应用访问路径 例如使用前缀 /admin/ VITE_APP_CONTEXT_PATH = '/' +# 请求接口是否加密 +IS_ENCRYPT = 'true' + # 监控地址 VITE_APP_MONITRO_ADMIN = '/admin/applications' diff --git a/ruoyi-ui-vue3/package.json b/ruoyi-ui-vue3/package.json index ad36a4f76..286cb2194 100644 --- a/ruoyi-ui-vue3/package.json +++ b/ruoyi-ui-vue3/package.json @@ -33,6 +33,7 @@ "devDependencies": { "@vitejs/plugin-vue": "3.1.0", "@vue/compiler-sfc": "3.2.45", + "crypto-js": "^4.1.1", "sass": "1.56.1", "unplugin-auto-import": "0.11.4", "vite": "3.2.3", diff --git a/ruoyi-ui-vue3/src/utils/aes.js b/ruoyi-ui-vue3/src/utils/aes.js new file mode 100644 index 000000000..e5633062a --- /dev/null +++ b/ruoyi-ui-vue3/src/utils/aes.js @@ -0,0 +1,39 @@ +import CryptoJS from 'crypto-js'; + +/** + * 随机生成aes 密钥 + * @returns {string} + */ +export const generateAesKey = () => { + return CryptoJS.enc.Utf8.parse(generateRandomString()); +}; + + +/** + * 使用密钥对数据进行加密 + * @param message + * @param aesKey + * @returns {string} + */ +export const encryptWithAes = (message, aesKey) => { + console.log(message, aesKey) + const encrypted = CryptoJS.AES.encrypt(message, aesKey, { + "mode": CryptoJS.mode.ECB, + "padding": CryptoJS.pad.Pkcs7 + }); + return encrypted.toString(); +}; + +/** + * 随机生成32位的字符串 + * @returns {string} + */ +const generateRandomString = () => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + const charactersLength = characters.length; + for (let i = 0; i < 32; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}; diff --git a/ruoyi-ui-vue3/src/utils/request.js b/ruoyi-ui-vue3/src/utils/request.js index 1da35094e..3995e8f8e 100644 --- a/ruoyi-ui-vue3/src/utils/request.js +++ b/ruoyi-ui-vue3/src/utils/request.js @@ -6,6 +6,9 @@ import { tansParams, blobValidate } from '@/utils/ruoyi' import cache from '@/plugins/cache' import { saveAs } from 'file-saver' import useUserStore from '@/store/modules/user' +import {encryptWithAes, generateAesKey} from "@/utils/aes"; +import {encrypt} from "@/utils/jsencrypt"; +import CryptoJS from "crypto-js"; let downloadLoadingInstance; // 是否显示重新登录 @@ -28,6 +31,8 @@ service.interceptors.request.use(config => { const isToken = (config.headers || {}).isToken === false // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + // 是否需要加密 + const isEncrypt = (config.headers || {}).isEncrypt === import.meta.env.IS_ENCRYPT if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } @@ -61,6 +66,13 @@ service.interceptors.request.use(config => { } } } + // 当开启参数加密 + if (isEncrypt && (config.method === 'post' || config.method === 'put')) { + // 生成一个 AES 密钥 + const aesKey = generateAesKey(); + config.headers['AES'] = encrypt(aesKey.toString(CryptoJS.enc.Base64)); + config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey); + } return config }, error => { console.log(error) diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index c5457c9a6..b9aa0d405 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -7,6 +7,9 @@ ENV = 'development' # 若依管理系统/开发环境 VUE_APP_BASE_API = '/dev-api' +# 请求接口是否加密 +IS_ENCRYPT = 'true' + # 应用访问路径 例如使用前缀 /admin/ VUE_APP_CONTEXT_PATH = '/' diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production index 1d2cbae6a..5a72452a1 100644 --- a/ruoyi-ui/.env.production +++ b/ruoyi-ui/.env.production @@ -7,6 +7,9 @@ ENV = 'production' # 若依管理系统/生产环境 VUE_APP_BASE_API = '/prod-api' +# 请求接口是否加密 +IS_ENCRYPT = 'true' + # 应用访问路径 例如使用前缀 /admin/ VUE_APP_CONTEXT_PATH = '/' diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 16924f395..2cef4f922 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -68,6 +68,7 @@ "chalk": "4.1.0", "compression-webpack-plugin": "5.0.2", "connect": "3.6.6", + "crypto-js": "^4.1.1", "eslint": "7.15.0", "eslint-plugin-vue": "7.2.0", "lint-staged": "10.5.3", diff --git a/ruoyi-ui/src/utils/aes.js b/ruoyi-ui/src/utils/aes.js new file mode 100644 index 000000000..e5633062a --- /dev/null +++ b/ruoyi-ui/src/utils/aes.js @@ -0,0 +1,39 @@ +import CryptoJS from 'crypto-js'; + +/** + * 随机生成aes 密钥 + * @returns {string} + */ +export const generateAesKey = () => { + return CryptoJS.enc.Utf8.parse(generateRandomString()); +}; + + +/** + * 使用密钥对数据进行加密 + * @param message + * @param aesKey + * @returns {string} + */ +export const encryptWithAes = (message, aesKey) => { + console.log(message, aesKey) + const encrypted = CryptoJS.AES.encrypt(message, aesKey, { + "mode": CryptoJS.mode.ECB, + "padding": CryptoJS.pad.Pkcs7 + }); + return encrypted.toString(); +}; + +/** + * 随机生成32位的字符串 + * @returns {string} + */ +const generateRandomString = () => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + const charactersLength = characters.length; + for (let i = 0; i < 32; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}; diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js index 850685cd6..1c2ebe376 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -6,6 +6,9 @@ import errorCode from '@/utils/errorCode' import { tansParams, blobValidate } from "@/utils/ruoyi"; import cache from '@/plugins/cache' import { saveAs } from 'file-saver' +import {encryptWithAes, generateAesKey} from "@/utils/aes"; +import {encrypt} from "@/utils/jsencrypt"; +import CryptoJS from "crypto-js"; let downloadLoadingInstance; // 是否显示重新登录 @@ -31,6 +34,8 @@ service.interceptors.request.use(config => { if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } + // 是否需要加密 + const isEncrypt = (config.headers || {}).isEncrypt === process.env.IS_ENCRYPT // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?' + tansParams(config.params); @@ -61,6 +66,13 @@ service.interceptors.request.use(config => { } } } + // 当开启参数加密 + if (isEncrypt && (config.method === 'post' || config.method === 'put')) { + // 生成一个 AES 密钥 + const aesKey = generateAesKey(); + config.headers['AES'] = encrypt(aesKey.toString(CryptoJS.enc.Base64)); + config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey); + } return config }, error => { console.log(error)