集成 jsencrypt 实现请求加密传输, 后端可在application.yml中开启, 前端可在.env配置文件IS_ENCRYPT属性修改.

This commit is contained in:
wdhcr 2023-06-20 16:41:24 +08:00
parent 1e69726d77
commit 6bc8d38416
15 changed files with 351 additions and 0 deletions

View File

@ -196,6 +196,16 @@ mybatis-encryptor:
publicKey:
privateKey:
# 数据加密
request-encryptor:
# 是否开启加密
enable: true
# AES 加密头标识
headerFlag: AES
# 公私钥 非对称算法的公私钥 如SM2RSA
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
# Swagger配置
swagger:
info:

View File

@ -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() {
}
}

View File

@ -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) {
}
};
}
}

View File

@ -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<CryptoFilter> cryptoFilterRegistration() {
FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new CryptoFilter());
registration.addUrlPatterns("/*");
registration.setName("cryptoFilter");
HashMap<String, String> 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")

View File

@ -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;
}

View File

@ -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'

View File

@ -7,6 +7,9 @@ VITE_APP_ENV = 'production'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'
# 请求接口是否加密
IS_ENCRYPT = 'true'
# 监控地址
VITE_APP_MONITRO_ADMIN = '/admin/applications'

View File

@ -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",

View File

@ -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;
};

View File

@ -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)

View File

@ -7,6 +7,9 @@ ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# 请求接口是否加密
IS_ENCRYPT = 'true'
# 应用访问路径 例如使用前缀 /admin/
VUE_APP_CONTEXT_PATH = '/'

View File

@ -7,6 +7,9 @@ ENV = 'production'
# 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api'
# 请求接口是否加密
IS_ENCRYPT = 'true'
# 应用访问路径 例如使用前缀 /admin/
VUE_APP_CONTEXT_PATH = '/'

View File

@ -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",

39
ruoyi-ui/src/utils/aes.js Normal file
View File

@ -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;
};

View File

@ -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)