前言
RuoYi-VUE-Plus 5.x的版本 已经完成了 前端加密传输给后端的代码,本文章只是将代码迁移到4.x 并完成 后端加密后给前端的代码,具体内容和加密方式都是使用作者:疯狂的狮子Li,欢迎大家使用
代码修改
前端
一、前端相关代码
引入步骤:
- 文件替换:将Utils 下面的代码进行替换,根据原有模块的功能 需要加入crypto、jsencrypt、request几个文件
- 配置密钥:jsencrypt里面的公钥和私钥需要和后端的配置文件一致起来
- 引入依赖:package.json文件下 引入npm 依赖 “crypto-js”: “^4.1.1”,
代码解释:
**request**: 请求文件,里面包含了请求加密和返回参数解密,通过判断header头中是否存在固定值来确定
**crypto** :AES、Base64 加解密文件主要是对参数进行加解密的方式
**jsencrypt**: RSA加解密文件 主要是对密钥进行加解密
密钥加密流程:
1. 生成32位AES密码
2. Base64 加密AES密码
3. 利用RSA将 加密后的AES密码进行加密
4. 传输给后端
数据加密流程:
使用32位AES密码作为key,加密数据 encryptWithAes(JSON.stringify(config.data), aesKey)
代码
package.json
"crypto-js": "^4.1.1",
src/utils 下 crypto.js
import CryptoJS from 'crypto-js';
/**
* 随机生成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;
};
/**
* 随机生成aes 密钥
* @returns {string}
*/
export const generateAesKey = () => {
return CryptoJS.enc.Utf8.parse(generateRandomString());
};
/**
* 加密base64
* @returns {string}
*/
export const encryptBase64 = (str) => {
return CryptoJS.enc.Base64.stringify(str);
};
/**
* 解密base64
* @returns {string}
*/
export const decryptBase64 = (str) => {
var words = CryptoJS.enc.Base64.parse(str);
return CryptoJS.enc.Utf8.stringify(words);
};
/**
* 使用密钥对数据进行加密
* @param message
* @param aesKey
* @returns {string}
*/
export const encryptWithAes = (message, aesKey) => {
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
};
/**
* 使用密钥对数据进行加密
* @param message
* @param aesKey
* @returns {string}
*/
export const decryptWithAes = (message, aesKey) => {
const keyBytes = CryptoJS.enc.Utf8.parse(aesKey);
const decrypt = CryptoJS.AES.decrypt(message, keyBytes, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return decrypt.toString(CryptoJS.enc.Utf8)
};
3.jsencrypt.js
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQdAQ==自己生成密钥对 这里的我删除了'
const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
'7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
'UP8iWi1Qw0Y='
// 加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
4.request.js
import axios from 'axios'
import {Loading, Message, MessageBox, Notification} from 'element-ui'
import store from '@/store'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import {blobValidate, tansParams} from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import {saveAs} from 'file-saver'
import {decrypt, encrypt} from "@/utils/jsencrypt";
import {decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey} from '@/utils/crypto';
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = {show: false};
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 对应国际化资源文件后缀
axios.defaults.headers['Content-Language'] = 'zh_CN'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API, // 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
// 是否需要加密
const isEncrypt = (config.headers || {}).isEncrypt === true;
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
// 当开启参数加密
if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
// 生成一个 AES 密钥
const aesKey = generateAesKey();
console.log(aesKey)
config.headers['encrypt-key'] = encrypt(encryptBase64(aesKey));
config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
}
// FormData数据去请求头Content-Type
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
return config;
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
if (res.headers && res.headers['encrypt-key']) {
const encryptByRsa = res.headers['encrypt-key']
//取出aes 加密后的key 然后解密
const encryptAesByBase64 = decrypt(encryptByRsa)
const aesPassword = decryptBase64(encryptAesByBase64) //eDBLQjEyQlBtZ1Y0ZjBLam9MZnJqUXNDd0I3dkJpenA=
res.data = JSON.parse(decryptWithAes(res.data, aesPassword));
}
//解密出现错误转json 提示错误
if (typeof res.data === 'string') {
res.data = JSON.parse(res.data)
}
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'
}).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({message: msg, type: 'error'})
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({message: msg, type: 'warning'})
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({title: msg})
return Promise.reject('error')
} else {
return res.data
}
}, error => {
console.log('err' + error)
let {message} = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({message: message, type: 'error', duration: 5 * 1000})
return Promise.reject(error)
})
// 通用下载方法
export function download(url, params, filename, config) {
downloadLoadingInstance = Loading.service({text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)",})
return service.post(url, params, {
transformRequest: [(params) => {
return tansParams(params)
}], headers: {'Content-Type': 'application/x-www-form-urlencoded'}, responseType: 'blob', ...config
}).then(async (data) => {
const isBlob = blobValidate(data);
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
Message.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
})
}
export default service
上面的代码基本是作者自己的可以参考5.X的代码
后端
二、后端相关代码
引入步骤:
-
文件替换:后端文件 包括:加解密方法、拦截器、配置文件,按照文件位置替换进去,配置文件中加了api-decrypt: 配置参数
-
配置密钥:可以通过互联网生成的key 需要与前端密钥相同,否则会导致无法解密
代码解释:
EncryptFilter :判断是否是排除加密路由,进行加密 具体看代码
密钥加密流程:
- 生成32位AES密码
- Base64 加密AES密码
- 利用RSA将 加密后的AES密码进行加密
- 传输给后端
数据加密流程:
使用32位AES密码作为key,加密数据 EncryptUtils.encryptByAes(responseBody, aesPassword);
代码
1.application.yml
# api接口加密
api-decrypt:
# 是否开启全局接口加密
enabled: true
# AES 加密头标识
headerFlag: encrypt-key
# 公私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsWqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
# 不需要加密路径
encryptExcludes: /monitor/*,/tool/*,/captchaImage
2.过滤器
package com.ruoyi.framework.encrypt.filter;//package com.yawei.gateway.filter;
/**
* 描述
*
* @author :suiquantong
* @date : 2023/3/15 9:22
*/
import cn.hutool.core.util.RandomUtil;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.utils.EncryptUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.properties.ApiDecryptProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 解密拦过滤器
*
* @author yawei
*/
public class EncryptFilter implements Filter {
private final ApiDecryptProperties properties;
/**
* 排除链接
*/
public List<String> excludes = new ArrayList<>();
public EncryptFilter(ApiDecryptProperties properties) {
this.properties = properties;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes)) {
String[] url = tempExcludes.split(StringUtils.SEPARATOR);
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String url = servletRequest.getServletPath();
/*
* 1.过滤不需要加密url (可选)
* 2.过滤非Json 格式数据
*/
if (!StringUtils.matches(url, excludes)) {
// if (false) {
EncryptResponseWrapper responseWrapper = new EncryptResponseWrapper((HttpServletResponse) response);
//执行业务逻辑 交给下一个过滤器或servlet处理
chain.doFilter(request, responseWrapper);
System.out.println("responseWrapper.getContentType() = " + responseWrapper.getContentType());
if (!isJsonResponse(response)) {
System.out.println("response.getContentType() = " + response.getContentType());
//不需要加密
chain.doFilter(request, response);
}else {
try {
//获取返回体
byte[] responseData = responseWrapper.getResponseData();
//设置响应内容格式,防止解析响应内容时出错
// responseWrapper.setContentType("text/plain;charset=UTF-8");
String responseBody = new String(responseData, StandardCharsets.UTF_8);
//生成aes密码 采用res加密 与前端相同逻辑
//生成aes 32位随机码,
String aesPassword = RandomUtil.randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32);
//加密aes 密钥
String encryptAesByBase64 = EncryptUtils.encryptByBase64(aesPassword);
String encryptByRsa = EncryptUtils.encryptByRsa(encryptAesByBase64, properties.getPublicKey());
//绑定返回头标识
responseWrapper.setHeader(properties.getHeaderFlag(), encryptByRsa);
//加密内容
String encryptBody = EncryptUtils.encryptByAes(responseBody, aesPassword);
PrintWriter out = response.getWriter();
out.print(encryptBody);
out.flush();
out.close();
} catch (Exception e) {
try {
getFailResponse(responseWrapper);
} catch (IOException ioException) {
ioException.printStackTrace();
}
e.printStackTrace();
}
}
} else {
//不需要加密
chain.doFilter(request, response);
}
}
/**
* 有错误相应返回-44
*
* @param response
* @throws IOException
*/
private void getFailResponse(HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
out = response.getWriter();
out.write("{\n" + " \"status\":" + HttpStatus.ENCRYPT_FAIL + ",\n" + " \"message\": 解密出现异常!,\n" + " \"data\": []\n" + "}");
//加密后的错误消息
// out.write("+D+JO8tuwkrNbxnTTLdqStifmQceT+LlYETnIG/JZKrbAn+gIiqIp3VbzBV1y6R8B7aY53VM2xHa7cY3Osbnqw==");
out.flush();
out.close();
}
/**
* 是否是Json请求
*/
public boolean isJsonResponse(ServletResponse response) {
String contentType = response.getContentType();
return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
}
}
package com.ruoyi.framework.encrypt.filter;
import cn.hutool.core.io.IoUtil;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.utils.EncryptUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @Description: 响应包装类
* @Date: 2020/5/26 16:29
*/
public class EncryptResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = null;
private ServletOutputStream out = null;
private PrintWriter writer = null;
public EncryptResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
buffer = new ByteArrayOutputStream();// 真正存储数据的流
out = new WapperedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer,this.getCharacterEncoding()));
}
/** 重载父类获取outputstream的方法 */
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
/** 重载父类获取writer的方法 */
@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
return writer;
}
/** 重载父类获取flushBuffer的方法 */
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
/** 将out、writer中的数据强制输出到WapperedResponse的buffer里面,否则取不到数据 */
public byte[] getResponseData() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
/** 内部类,对ServletOutputStream进行包装 */
private class WapperedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = null;
public WapperedOutputStream(ByteArrayOutputStream stream)
throws IOException {
bos = stream;
}
@Override
public void write(int b) throws IOException {
bos.write(b);
}
@Override
public void write(byte[] b) throws IOException {
bos.write(b, 0, b.length);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
public static ByteArrayOutputStream toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024*4];
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
return output;
}
}
3.具体内容太多了 直接上附件吧