mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-09-21 02:25:56 +08:00
Pre Merge pull request !346 from ltyzzz/jsencrypt
This commit is contained in:
commit
0e3d3b29fc
@ -197,6 +197,11 @@ mybatis-encryptor:
|
||||
publicKey:
|
||||
privateKey:
|
||||
|
||||
# 接口加密
|
||||
interface-encryptor:
|
||||
enable: true
|
||||
secret: abcdefghijklmnop
|
||||
|
||||
# Swagger配置
|
||||
swagger:
|
||||
info:
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
package com.ruoyi.common.filter;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.encrypt.EncryptContext;
|
||||
import com.ruoyi.common.encrypt.encryptor.AesEncryptor;
|
||||
import com.ruoyi.common.enums.AlgorithmType;
|
||||
import com.ruoyi.common.enums.EncodeType;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 加解密
|
||||
*
|
||||
* @author ltyzzz
|
||||
* @email ltyzzz2000@gmail.com
|
||||
* @date 2023/4/27 01:15
|
||||
*/
|
||||
public class EncryptFilter implements Filter {
|
||||
|
||||
private String secret;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
secret = filterConfig.getInitParameter("secret");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
HttpServletResponse resp = (HttpServletResponse) response;
|
||||
String uri = req.getRequestURI();
|
||||
System.out.println("加解密过滤器," + uri);
|
||||
|
||||
EncryptContext encryptContext = new EncryptContext();
|
||||
encryptContext.setPassword(secret);
|
||||
encryptContext.setAlgorithm(AlgorithmType.AES);
|
||||
encryptContext.setEncode(EncodeType.BASE64);
|
||||
AesEncryptor aesEncryptor = new AesEncryptor(encryptContext);
|
||||
|
||||
byte[] body;
|
||||
if (req.getContentLength() > 0) {
|
||||
String reqBody = getRequestBody(req);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map map = objectMapper.readValue(reqBody, Map.class);
|
||||
String encryptData = (String) map.get("encryptData");
|
||||
String data = aesEncryptor.decrypt(encryptData);
|
||||
System.out.println(data);
|
||||
body = data.getBytes();
|
||||
} else {
|
||||
body = IoUtil.readBytes(request.getInputStream(), false);
|
||||
}
|
||||
|
||||
EncryptRequestWrapper reqWrapper = new EncryptRequestWrapper(req, body);
|
||||
EncryptResponseWrapper respWrapper = new EncryptResponseWrapper(resp);
|
||||
chain.doFilter(reqWrapper, respWrapper);
|
||||
byte[] resData = respWrapper.getResponseData();
|
||||
String encrypt = aesEncryptor.encrypt(new String(resData), EncodeType.BASE64);
|
||||
PrintWriter out = response.getWriter();
|
||||
out.print(encrypt);
|
||||
out.flush();
|
||||
out.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Filter.super.destroy();
|
||||
}
|
||||
|
||||
private String getRequestBody(ServletRequest request) throws IOException {
|
||||
BufferedReader reader = request.getReader();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package com.ruoyi.common.filter;
|
||||
|
||||
import com.ruoyi.common.encrypt.encryptor.AesEncryptor;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author ltyzzz
|
||||
* @email ltyzzz2000@gmail.com
|
||||
* @date 2023/4/27 20:58
|
||||
*/
|
||||
public class EncryptRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private HttpServletRequest request;
|
||||
private String secret;
|
||||
private AesEncryptor aesEncryptor;
|
||||
|
||||
private byte[] body;
|
||||
|
||||
public EncryptRequestWrapper(HttpServletRequest request, byte[] body) throws IOException {
|
||||
super(request);
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
return new ServletInputStream() {
|
||||
private final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return inputStream.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.ruoyi.common.filter;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* @author ltyzzz
|
||||
* @email ltyzzz2000@gmail.com
|
||||
* @date 2023/4/28 00:15
|
||||
*/
|
||||
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()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() throws UnsupportedEncodingException {
|
||||
return writer;
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import com.ruoyi.common.filter.EncryptFilter;
|
||||
import com.ruoyi.common.filter.RepeatableFilter;
|
||||
import com.ruoyi.common.filter.XssFilter;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.config.properties.InterfaceEncryptProperties;
|
||||
import com.ruoyi.framework.config.properties.XssProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@ -25,6 +27,9 @@ public class FilterConfig {
|
||||
@Autowired
|
||||
private XssProperties xssProperties;
|
||||
|
||||
@Autowired
|
||||
private InterfaceEncryptProperties interfaceEncryptProperties;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
|
||||
@ -52,4 +57,19 @@ public class FilterConfig {
|
||||
return registration;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "interface-encryptor.enable", havingValue = "true")
|
||||
public FilterRegistrationBean encryptFilterRegistration() {
|
||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
registration.setFilter(new EncryptFilter());
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setName("encryptFilter");
|
||||
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
|
||||
Map<String, String> initParameters = new HashMap<String, String>();
|
||||
initParameters.put("secret", interfaceEncryptProperties.getSecret());
|
||||
registration.setInitParameters(initParameters);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.ruoyi.framework.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author ltyzzz
|
||||
* @email ltyzzz2000@gmail.com
|
||||
* @date 2023/4/27 16:55
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "interface-encryptor")
|
||||
public class InterfaceEncryptProperties {
|
||||
|
||||
/**
|
||||
* 过滤开关
|
||||
*/
|
||||
private Boolean enable;
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
private String secret;
|
||||
}
|
||||
@ -39,6 +39,7 @@
|
||||
"axios": "0.24.0",
|
||||
"clipboard": "2.0.8",
|
||||
"core-js": "3.25.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"echarts": "5.4.0",
|
||||
"element-ui": "2.15.13",
|
||||
"file-saver": "2.0.5",
|
||||
|
||||
20
ruoyi-ui/src/utils/crypto.js
Normal file
20
ruoyi-ui/src/utils/crypto.js
Normal file
@ -0,0 +1,20 @@
|
||||
import CryptoJS from "crypto-js";
|
||||
const key = CryptoJS.enc.Utf8.parse("abcdefghijklmnop")
|
||||
|
||||
// 加密
|
||||
export function encryptAES(data) {
|
||||
var encrypted = CryptoJS.AES.encrypt(data, key, {
|
||||
"mode": CryptoJS.mode.ECB,
|
||||
"padding": CryptoJS.pad.Pkcs7
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
// 解密
|
||||
export function decryptAES(ciphertext) {
|
||||
var decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
|
||||
"mode": CryptoJS.mode.ECB,
|
||||
"padding": CryptoJS.pad.Pkcs7
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
@ -1,15 +1,16 @@
|
||||
import axios from 'axios'
|
||||
import { Notification, MessageBox, Message, Loading } from 'element-ui'
|
||||
import {Notification, MessageBox, Message, Loading} from 'element-ui'
|
||||
import store from '@/store'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import {getToken} from '@/utils/auth'
|
||||
import errorCode from '@/utils/errorCode'
|
||||
import { tansParams, blobValidate } from "@/utils/ruoyi";
|
||||
import {tansParams, blobValidate} from "@/utils/ruoyi";
|
||||
import cache from '@/plugins/cache'
|
||||
import { saveAs } from 'file-saver'
|
||||
import {saveAs} from 'file-saver'
|
||||
import {encryptAES, decryptAES} from "@/utils/crypto";
|
||||
|
||||
let downloadLoadingInstance;
|
||||
// 是否显示重新登录
|
||||
export let isRelogin = { show: false };
|
||||
export let isRelogin = {show: false};
|
||||
|
||||
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||
// 对应国际化资源文件后缀
|
||||
@ -21,6 +22,8 @@ const service = axios.create({
|
||||
// 超时
|
||||
timeout: 10000
|
||||
})
|
||||
// 是否需要加密请求
|
||||
const isEncrypt = true
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
@ -61,43 +64,57 @@ service.interceptors.request.use(config => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEncrypt && config.data) {
|
||||
let s = JSON.stringify(config.data)
|
||||
let encryptData = encryptAES(s)
|
||||
config.data = {
|
||||
encryptData: encryptData
|
||||
}
|
||||
}
|
||||
return config
|
||||
}, error => {
|
||||
console.log(error)
|
||||
Promise.reject(error)
|
||||
console.log(error)
|
||||
Promise.reject(error)
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(res => {
|
||||
if (isEncrypt) {
|
||||
res.data = JSON.parse(decryptAES(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') {
|
||||
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(() => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
isRelogin.show = false;
|
||||
});
|
||||
}
|
||||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
||||
} else if (code === 500) {
|
||||
Message({ message: msg, type: 'error' })
|
||||
Message({message: msg, type: 'error'})
|
||||
return Promise.reject(new Error(msg))
|
||||
} else if (code === 601) {
|
||||
Message({ message: msg, type: 'warning' })
|
||||
Message({message: msg, type: 'warning'})
|
||||
return Promise.reject('error')
|
||||
} else if (code !== 200) {
|
||||
Notification.error({ title: msg })
|
||||
Notification.error({title: msg})
|
||||
return Promise.reject('error')
|
||||
} else {
|
||||
return res.data
|
||||
@ -105,7 +122,7 @@ service.interceptors.response.use(res => {
|
||||
},
|
||||
error => {
|
||||
console.log('err' + error)
|
||||
let { message } = error;
|
||||
let {message} = error;
|
||||
if (message == "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message.includes("timeout")) {
|
||||
@ -113,17 +130,23 @@ service.interceptors.response.use(res => {
|
||||
} else if (message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
Message({ message: message, type: 'error', duration: 5 * 1000 })
|
||||
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)", })
|
||||
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' },
|
||||
transformRequest: [(params) => {
|
||||
return tansParams(params)
|
||||
}],
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
responseType: 'blob',
|
||||
...config
|
||||
}).then(async (data) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user