集成ai部分
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.common.constant;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 加解密配置常量
|
||||
*/
|
||||
public class EncryptConstants {
|
||||
|
||||
/**
|
||||
* 是否开启加解密功能
|
||||
*/
|
||||
public static final boolean ENCRYPT_ENABLED = true;
|
||||
|
||||
/**
|
||||
* 需要加解密的URL路径模式
|
||||
*/
|
||||
public static final List<String> URL_PATTERNS = Arrays.asList(
|
||||
/*"/app/login",
|
||||
"/app/user/resume",
|
||||
"/app/user/experience/edit",
|
||||
"/app/user/experience/delete",
|
||||
"/app/user/experience/getSingle/*",
|
||||
"/app/user/experience/list",
|
||||
"/login",
|
||||
"/system/user/resetPwd",
|
||||
"/system/user/list",
|
||||
"/system/user",
|
||||
"/cms/appUser/list",
|
||||
"/cms/appUser/getResumeList",
|
||||
"/cms/appUser/getResumeDetail/*",
|
||||
"/app/alipay/scanLogin",
|
||||
"/app/user/cert"*/
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.ruoyi.common.constant;
|
||||
|
||||
public class SM4Constants {
|
||||
|
||||
public static final String SM4_KET = "86C63180C1306ABC4D8F989E0A0BC9F3";
|
||||
}
|
||||
@@ -56,4 +56,8 @@ public enum BusinessType
|
||||
* 清空数据
|
||||
*/
|
||||
CLEAN,
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
QUERY
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.ruoyi.common.filter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.constant.EncryptConstants;
|
||||
import com.ruoyi.common.constant.SM4Constants;
|
||||
import com.ruoyi.common.utils.EncryptHttpServletResponseWrapper;
|
||||
import com.ruoyi.common.utils.SM4Utils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class EncryptResponseFilter implements Filter {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
|
||||
// 检查是否开启加解密功能
|
||||
if (!EncryptConstants.ENCRYPT_ENABLED) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是SSE流式响应 - 不处理
|
||||
if (isSseResponse(httpRequest, httpResponse)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查URL是否需要加密响应
|
||||
if (!needProcess(httpRequest)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 包装响应
|
||||
EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(httpResponse);
|
||||
chain.doFilter(request, responseWrapper);
|
||||
|
||||
// 获取原始响应内容
|
||||
byte[] content = responseWrapper.getContent();
|
||||
String originalResponse = new String(content, StandardCharsets.UTF_8);
|
||||
|
||||
// 加密响应内容
|
||||
if (StringUtils.isNotBlank(originalResponse) && !originalResponse.trim().isEmpty()) {
|
||||
String encryptedResponse = SM4Utils.encryptEcb(SM4Constants.SM4_KET, originalResponse);
|
||||
|
||||
// 构建加密响应
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("encrypted", true);
|
||||
result.put("encryptedData", encryptedResponse);
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
String finalResponse = objectMapper.writeValueAsString(result);
|
||||
|
||||
// 设置响应
|
||||
byte[] encryptedBytes = finalResponse.getBytes(StandardCharsets.UTF_8);
|
||||
httpResponse.setContentLength(encryptedBytes.length);
|
||||
httpResponse.setContentType("application/json;charset=UTF-8");
|
||||
httpResponse.getOutputStream().write(encryptedBytes);
|
||||
} else {
|
||||
// 空响应直接返回空
|
||||
httpResponse.getOutputStream().write(new byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否需要处理
|
||||
*/
|
||||
private boolean needProcess(HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
// 检查URL是否匹配需要加解密的模式
|
||||
for (String pattern : EncryptConstants.URL_PATTERNS) {
|
||||
if (pathMatcher.match(pattern, requestURI)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是SSE流式响应
|
||||
*/
|
||||
private boolean isSseResponse(HttpServletRequest request, HttpServletResponse response) {
|
||||
String path = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
// 检查是否是聊天接口
|
||||
if ("/app/chat/chat".equals(path) && "POST".equalsIgnoreCase(method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查Accept头是否包含text/event-stream
|
||||
String acceptHeader = request.getHeader("Accept");
|
||||
if (acceptHeader != null && acceptHeader.contains("text/event-stream")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查响应头是否已经设置为text/event-stream
|
||||
String contentType = response.getContentType();
|
||||
if (contentType != null && contentType.contains("text/event-stream")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.ruoyi.common.filter;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.constant.EncryptConstants;
|
||||
import com.ruoyi.common.constant.SM4Constants;
|
||||
import com.ruoyi.common.utils.EncryptHttpServletRequestWrapper;
|
||||
import com.ruoyi.common.utils.EncryptHttpServletResponseWrapper;
|
||||
import com.ruoyi.common.utils.SM4Utils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 请求包装过滤器 - 处理请求解密和包装
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class RequestWrapperFilter implements Filter {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final PathMatcher pathMatcher = new AntPathMatcher();
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
|
||||
// 检查是否开启加解密功能
|
||||
if (!EncryptConstants.ENCRYPT_ENABLED) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是SSE流式响应 - 不处理
|
||||
if (isSseResponse(httpRequest, httpResponse)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查URL是否需要解密
|
||||
if (needProcess(httpRequest)) {
|
||||
// 处理请求解密
|
||||
String method = httpRequest.getMethod();
|
||||
if("POST".equalsIgnoreCase(method)) {
|
||||
HttpServletRequest processedRequest = processBodyRequest(httpRequest);
|
||||
chain.doFilter(processedRequest, response);
|
||||
}else{
|
||||
EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(httpResponse);
|
||||
String forwardUrl = buildGetRequestURI(httpRequest);
|
||||
if (StringUtils.isNotBlank(forwardUrl)) {
|
||||
log.info("GET请求解密后转发URL:{}", forwardUrl);
|
||||
// 服务器内部转发
|
||||
request.setAttribute("ENCRYPT_PROCESSED", Boolean.TRUE);
|
||||
RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUrl);
|
||||
dispatcher.forward(request, responseWrapper);
|
||||
|
||||
// 2. 监听/处理转发后的响应(核心逻辑:从包装器中获取响应数据)
|
||||
byte[] content = responseWrapper.getContent();
|
||||
String originalResponse = new String(content, StandardCharsets.UTF_8);
|
||||
if (StringUtils.isNotBlank(originalResponse) && !originalResponse.trim().isEmpty()) {
|
||||
String encryptedResponse = SM4Utils.encryptEcb(SM4Constants.SM4_KET, originalResponse);
|
||||
|
||||
// 构建加密响应
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("encrypted", true);
|
||||
result.put("encryptedData", encryptedResponse);
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
String finalResponse = objectMapper.writeValueAsString(result);
|
||||
|
||||
// 设置响应
|
||||
byte[] encryptedBytes = finalResponse.getBytes(StandardCharsets.UTF_8);
|
||||
httpResponse.setContentLength(encryptedBytes.length);
|
||||
httpResponse.setContentType("application/json;charset=UTF-8");
|
||||
httpResponse.getOutputStream().write(encryptedBytes);
|
||||
} else {
|
||||
// 空响应直接返回空
|
||||
httpResponse.getOutputStream().write(new byte[0]);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不需要解密,直接放行
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否需要处理
|
||||
*/
|
||||
private boolean needProcess(HttpServletRequest request) {
|
||||
if (Boolean.TRUE.equals(request.getAttribute("ENCRYPT_PROCESSED"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
// 检查URL是否匹配需要加解密的模式
|
||||
for (String pattern : EncryptConstants.URL_PATTERNS) {
|
||||
if (pathMatcher.match(pattern, requestURI)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是SSE流式响应
|
||||
*/
|
||||
private boolean isSseResponse(HttpServletRequest request, HttpServletResponse response) {
|
||||
String path = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
// 检查是否是聊天接口
|
||||
if ("/app/chat/chat".equals(path) && "POST".equalsIgnoreCase(method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查Accept头是否包含text/event-stream
|
||||
String acceptHeader = request.getHeader("Accept");
|
||||
if (acceptHeader != null && acceptHeader.contains("text/event-stream")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查响应头是否已经设置为text/event-stream
|
||||
String contentType = response.getContentType();
|
||||
if (contentType != null && contentType.contains("text/event-stream")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HttpServletRequest processBodyRequest(HttpServletRequest request) throws IOException {
|
||||
String body = getRequestBody(request);
|
||||
log.info("过滤器 - 原始请求体: " + body);
|
||||
|
||||
if (StringUtils.isNotBlank(body)) {
|
||||
try {
|
||||
// 解析请求体,提取encryptedData字段
|
||||
JsonNode jsonNode = objectMapper.readTree(body);
|
||||
JsonNode encryptedDataNode = jsonNode.get("encryptedData");
|
||||
|
||||
if (encryptedDataNode != null && encryptedDataNode.isTextual()) {
|
||||
String encryptedData = encryptedDataNode.asText();
|
||||
// 解密 encryptedData
|
||||
String decryptedData = SM4Utils.decryptEcb(SM4Constants.SM4_KET, encryptedData);
|
||||
|
||||
// 使用解密后的数据创建包装请求
|
||||
return new EncryptHttpServletRequestWrapper(request, decryptedData);
|
||||
} else {
|
||||
log.info("请求体中未找到encryptedData字段,直接返回原始请求");
|
||||
return request;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("POST请求体解密失败: " + e.getMessage());
|
||||
// 解密失败时返回原始请求,让业务层处理
|
||||
return request;
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private String getRequestBody(HttpServletRequest request) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
BufferedReader bufferedReader = null;
|
||||
try {
|
||||
InputStream inputStream = request.getInputStream();
|
||||
if (inputStream != null) {
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
char[] charBuffer = new char[128];
|
||||
int bytesRead;
|
||||
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
|
||||
stringBuilder.append(charBuffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (bufferedReader != null) {
|
||||
bufferedReader.close();
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private String buildGetRequestURI(HttpServletRequest request) {
|
||||
String baseUrl = request.getRequestURI(); // 完整基础URL
|
||||
String encryptedData = request.getParameter("encryptedData");
|
||||
|
||||
if (StringUtils.isBlank(encryptedData)) {
|
||||
log.info("GET请求未携带encryptedData参数,返回原始URL");
|
||||
return null; // 无加密参数,不处理
|
||||
}
|
||||
|
||||
try {
|
||||
// 解密参数(增加异常捕获,避免解密失败导致URL拼接异常)
|
||||
String decryptedData = SM4Utils.decryptEcb(SM4Constants.SM4_KET, encryptedData);
|
||||
if (StringUtils.isBlank(decryptedData)) {
|
||||
log.error("GET请求encryptedData解密后为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject object = JSONObject.parseObject(decryptedData);
|
||||
if (object.isEmpty()) {
|
||||
log.info("解密后的参数为空,返回原始URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 拼接参数(用StringBuilder,避免字符串频繁拼接)
|
||||
StringBuilder params = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : object.entrySet()) { // 用entrySet更高效
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
// 跳过空值和空key
|
||||
if (StringUtils.isBlank(key) || Objects.isNull(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理参数值:数组/嵌套JSON转为JSON字符串,普通类型直接转字符串
|
||||
String paramValue = String.valueOf(value);
|
||||
|
||||
// URL编码(处理中文、特殊字符,如空格、&、=等)
|
||||
String encodedValue = URLEncoder.encode(paramValue, "UTF-8");
|
||||
if (params.length() > 0) {
|
||||
params.append("&");
|
||||
}
|
||||
params.append(key).append("=").append(encodedValue);
|
||||
}
|
||||
|
||||
// 拼接完整URL(仅当有参数时才加?)
|
||||
if (params.length() > 0) {
|
||||
return baseUrl + "?" + params.toString();
|
||||
} else {
|
||||
log.info("解密后的参数均为空,返回原始URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("GET请求参数解密/拼接失败", e);
|
||||
return null; // 异常时返回null,走原始请求
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
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 EncryptHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
private final String body;
|
||||
|
||||
public EncryptHttpServletRequestWrapper(HttpServletRequest request, String body) {
|
||||
super(request);
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return this.body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
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 EncryptHttpServletResponseWrapper extends HttpServletResponseWrapper {
|
||||
private ByteArrayOutputStream buffer;
|
||||
private ServletOutputStream out;
|
||||
private PrintWriter writer;
|
||||
|
||||
public EncryptHttpServletResponseWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
buffer = new ByteArrayOutputStream();
|
||||
out = new WrappedOutputStream(buffer);
|
||||
writer = new PrintWriter(new OutputStreamWriter(buffer, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() throws IOException {
|
||||
return writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
if (out != null) {
|
||||
out.flush();
|
||||
}
|
||||
if (writer != null) {
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getContent() throws IOException {
|
||||
flushBuffer();
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
|
||||
private static class WrappedOutputStream extends ServletOutputStream {
|
||||
private ByteArrayOutputStream buffer;
|
||||
|
||||
public WrappedOutputStream(ByteArrayOutputStream buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
buffer.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
}
|
||||
}
|
||||
}
|
||||
211
ruoyi-common/src/main/java/com/ruoyi/common/utils/SM4Utils.java
Normal file
211
ruoyi-common/src/main/java/com/ruoyi/common/utils/SM4Utils.java
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user