diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index da4154526..5df1f1a37 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -197,6 +197,11 @@ mybatis-encryptor: publicKey: privateKey: +# 接口加密 +interface-encryptor: + enable: true + secret: abcdefghijklmnop + # Swagger配置 swagger: info: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptFilter.java new file mode 100644 index 000000000..891e349d1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptFilter.java @@ -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(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptRequestWrapper.java new file mode 100644 index 000000000..eac97eafa --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptRequestWrapper.java @@ -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) { + + } + }; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseWrapper.java new file mode 100644 index 000000000..3c833b68e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseWrapper.java @@ -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) { + + } + } +} 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..7f206e362 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.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 initParameters = new HashMap(); + initParameters.put("secret", interfaceEncryptProperties.getSecret()); + registration.setInitParameters(initParameters); + return registration; + } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/InterfaceEncryptProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/InterfaceEncryptProperties.java new file mode 100644 index 000000000..7a5590848 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/InterfaceEncryptProperties.java @@ -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; +} diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 16924f395..de11f9456 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -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", diff --git a/ruoyi-ui/src/utils/crypto.js b/ruoyi-ui/src/utils/crypto.js new file mode 100644 index 000000000..590ad40af --- /dev/null +++ b/ruoyi-ui/src/utils/crypto.js @@ -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); +} diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js index 850685cd6..429ad394d 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -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) => {