这里写自定义目录标题
- 前言
- 代码修改
- 前端
- 引入步骤:
- 代码解释:
- 代码
- 后端
- 引入步骤:
- 代码解释:
- 代码
前言
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/netrsakeypairconst 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 => {// 是否需要设置 tokenconst 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-Typeif (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;}@Overridepublic 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]);}}}@Overridepublic 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的方法 */@Overridepublic ServletOutputStream getOutputStream() throws IOException {return out;}/** 重载父类获取writer的方法 */@Overridepublic PrintWriter getWriter() throws UnsupportedEncodingException {return writer;}/** 重载父类获取flushBuffer的方法 */@Overridepublic void flushBuffer() throws IOException {if (out != null) {out.flush();}if (writer != null) {writer.flush();}}@Overridepublic 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;}@Overridepublic void write(int b) throws IOException {bos.write(b);}@Overridepublic void write(byte[] b) throws IOException {bos.write(b, 0, b.length);}@Overridepublic boolean isReady() {return false;}@Overridepublic 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.具体内容太多了 直接上附件吧