接口数据加密传输
加密算法采用了对称加密AES算法
前端
引入 crypto-js
# 使用npm引入
npm install crypto-js --save
# 如果npm引入失败,使用cnpm
cnpm install crypto-js --save
然后写一个js,方便调用
AesUtil.js
import CryptoJS from 'crypto-js';
/**
* 随机生成16位的AES密钥
* @returns {string}
*/
export const getAESKey = () => {
var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
var n = 16;
var res = "";
for (var i = 0; i < n; i++) {
var id = Math.ceil(Math.random() * 35);
res += chars[id];
}
return res;
}
/**
* 加密
* @param data
* @param aesKey
* @returns {string}
* @constructor
*/
export const encryptAes = (data, aesKey) => {
let key = CryptoJS.enc.Utf8.parse(aesKey);
let srcs = CryptoJS.enc.Utf8.parse(data);
let encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
console.log(encrypted.ciphertext)
return encrypted.ciphertext.toString();
};
/**
* 解密Aes
* @param data
* @param aesKey
* @returns {string}
*/
export const decryptAes = (data, aesKey) => {
let keyStr = aesKey ? aesKey : "1234567890abcdef";
var key = CryptoJS.enc.Utf8.parse(keyStr);
var decrypt = CryptoJS.AES.decrypt(data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
};
全局环境中配置两个参数,用来控制加密解密
// 这里的密钥必须要符合AES的规则,可以先调用AesUtil中的getAESKey 方法,将获取到的key复制粘贴到此处
VUE_APP_ENCRYPT_KEY = '密钥'
// 这里控制是否加密
VUE_APP_ENCRYPT_ENABLED = true
在request.js的请求和响应中拦截数据并处理
先在request.js最上面引入加密解密方法和环境变量信息
import {decryptAes, encryptAes} from "@/utils/AesUtil";
const aesKey = process.env.VUE_APP_ENCRYPT_KEY;
const encryptEnabled = process.env.VUE_APP_ENCRYPT_ENABLED === 'true';
配置请求拦截器:
在return config上方加上这段:
// 数据加密
if (encryptEnabled){
console.log("加密")
config.data =typeof config.data === "object" ? encryptAes(JSON.stringify(config.data), aesKey) : encryptAes(config.data, aesKey);
}
配置响应拦截器:
将拦截器换成这个
service.interceptors.response.use(res => {
let data;
// 数据解密
if (res.data instanceof Object||!encryptEnabled){
data = res.data
}else {
let dataStr = decryptAes(res.data,aesKey)
data = JSON.parse(dataStr)
}
// 未设置状态码则默认成功状态
const code = data.code || 200;
// 获取错误信息
const msg = errorCode[code] || data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return 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 = '/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 data
}
},
后端
在application.yml中配置过滤的接口,配置的接口不会加密
application.yml加入下面配置:
encrypt:
filterUrls: /common/*,/profile/upload/*
application-dev.yml和application-pro.yml配置是否开启加密及密钥
application-dev.yml和application-pro.yml都加入下面的配置:
encrypt:
enabled: true
key: 密钥
然后在framework模块增加请求和响应的自定义增强器:
自定义请求增强器DecryptionRequestWrapper
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;
/**
* 自定义请求增强器
*/
public class DecryptionRequestWrapper extends HttpServletRequestWrapper {
private String decryptedBody;
public DecryptionRequestWrapper(HttpServletRequest request, String decryptedBody) throws IOException {
super(request);
this.decryptedBody = decryptedBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8))));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
自定义响应增强器EncryptionResponseWrapper
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 自定义响应增强器
*/
public class EncryptionResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private boolean hasFlushed = false;
public EncryptionResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
};
}
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, StandardCharsets.UTF_8));
}
@Override
public void flushBuffer() throws IOException {
super.flushBuffer();
if (byteArrayOutputStream != null) {
byteArrayOutputStream.flush();
}
hasFlushed = true;
}
public byte[] getContentAsByteArray() throws IOException {
if (!hasFlushed) {
flushBuffer();
}
return byteArrayOutputStream.toByteArray();
}
}
在framework模块,增加接口过滤器
接口过滤器DecryptionFilter
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.diyi.framework.security.handle.DecryptionRequestWrapper;
import com.diyi.framework.security.handle.EncryptionResponseWrapper;
import lombok.Data;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 接口加密解密过滤器
*/
// 使用了@Data注解,这里需要引入一下lombok的包,也可以不引入,手写get和set方法,不过感觉比较乱
@Data
public class DecryptionFilter implements Filter {
private Boolean enabled;
private String key;
private List<String> filterUrls;
public DecryptionFilter(Boolean enabled, String key, List<String> filterUrls) {
this.enabled = enabled;
this.key = key;
this.filterUrls = filterUrls;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 获取请求的 URL
String requestUrl = httpRequest.getRequestURI();
// 检查请求的 URL 是否匹配过滤列表中的规则
if (!enabled||isUrlMatchFilterUrls(requestUrl)) {
// 如果匹配上,直接放行,不走加密解密逻辑
chain.doFilter(request, response);
return;
}
// 缓存请求体以便多次读取
String body = getRequestBody(httpRequest);
AES aes = SecureUtil.aes(key.getBytes());
if (!body.isEmpty()) {
body = body.replace("\"","");
// 假设body中的双引号已被去除或其他预处理
String decryptedBody = aes.decryptStr(body);
// 将解密后的请求体传入新的请求对象
DecryptionRequestWrapper wrappedRequest = new DecryptionRequestWrapper(httpRequest, decryptedBody);
// 使用自定义的HttpServletResponseWrapper来捕获响应体
EncryptionResponseWrapper wrappedResponse = new EncryptionResponseWrapper(httpResponse);
// 传递包装后的请求和响应对象
chain.doFilter(wrappedRequest, wrappedResponse);
// 在这里加密响应体
byte[] originalContent = wrappedResponse.getContentAsByteArray();
if (originalContent.length > 0) {
String originalContentStr = new String(originalContent, StandardCharsets.UTF_8);
String encryptedContent = aes.encryptBase64(originalContentStr);
// 将加密后的响应写回给客户端
httpResponse.getOutputStream().write(encryptedContent.getBytes(StandardCharsets.UTF_8));
httpResponse.getOutputStream().flush();
}
} else {
// 使用自定义的HttpServletResponseWrapper来捕获响应体
EncryptionResponseWrapper wrappedResponse = new EncryptionResponseWrapper(httpResponse);
// 传递包装后的请求和响应对象
chain.doFilter(request, wrappedResponse);
// 在这里加密响应体
byte[] originalContent = wrappedResponse.getContentAsByteArray();
if (originalContent.length > 0) {
String originalContentStr = new String(originalContent, StandardCharsets.UTF_8);
String encryptedContent = aes.encryptBase64(originalContentStr);
// 将加密后的响应写回给客户端
httpResponse.getOutputStream().write(encryptedContent.getBytes(StandardCharsets.UTF_8));
httpResponse.getOutputStream().flush();
}
}
}
private boolean isUrlMatchFilterUrls(String requestUrl) {
if (filterUrls == null) {
return false;
}
for (String filterUrl : filterUrls) {
if (isUrlMatchPattern(requestUrl, filterUrl)) {
return true;
}
}
return false;
}
private boolean isUrlMatchPattern(String requestUrl, String pattern) {
String[] requestSegments = requestUrl.split("/");
String[] patternSegments = pattern.split("/");
int requestIndex = 0;
int patternIndex = 0;
while (requestIndex < requestSegments.length && patternIndex < patternSegments.length) {
if ("*".equals(patternSegments[patternIndex])) {
patternIndex++;
// 处理连续的 * 通配符
while (patternIndex < patternSegments.length && "*".equals(patternSegments[patternIndex])) {
patternIndex++;
}
if (patternIndex == patternSegments.length) {
// 如果模式的最后是 *,则直接匹配成功
return true;
}
// 查找下一个匹配的路径段
while (requestIndex < requestSegments.length && !requestSegments[requestIndex].equals(patternSegments[patternIndex])) {
requestIndex++;
}
if (requestIndex == requestSegments.length) {
// 没有找到匹配的路径段
return false;
}
} else if (!requestSegments[requestIndex].equals(patternSegments[patternIndex])) {
return false;
}
requestIndex++;
patternIndex++;
}
// 检查是否所有路径段都匹配
return requestIndex == requestSegments.length && patternIndex == patternSegments.length;
}
private String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
bufferedReader = request.getReader();
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
// 忽略
}
}
}
return stringBuilder.toString();
}
@Override
public void destroy() {}
}
在framework模块,配置过滤器FilterConfig,使其生效
代码如下:
@Bean
public FilterRegistrationBean<DecryptionFilter> decryptionFilter(
@Value("${encrypt.enabled}") Boolean enabled,
@Value("${encrypt.key}") String key,
@Value("${encrypt.filterUrls}") List<String> filterUrls) {
FilterRegistrationBean<DecryptionFilter> registrationBean = new FilterRegistrationBean<>();
DecryptionFilter decryptionFilter = new DecryptionFilter(enabled, key,filterUrls);
registrationBean.setFilter(decryptionFilter);
registrationBean.addUrlPatterns("/*"); // 根据需要调整URL模式
return registrationBean;
}
到此,本篇文章代码部分已结束。