集成ai部分

This commit is contained in:
sh
2026-01-05 15:39:01 +08:00
parent deb775ff5c
commit a31fc4cc72
23 changed files with 1885 additions and 1 deletions

View File

@@ -120,6 +120,22 @@
<artifactId>UserAgentUtils</artifactId>
</dependency>
<!--奇安信密码机-->
<!--<dependency>
<groupId>org.quickssl</groupId>
<artifactId>quickapi-client-java</artifactId>
<version>1.5.11-SNAPSHOT</version>
<classifier>shaded</classifier>
<scope>compile</scope>
</dependency>-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.59</version>
<scope>compile</scope>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>

View File

@@ -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"*/
);
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.common.constant;
public class SM4Constants {
public static final String SM4_KET = "86C63180C1306ABC4D8F989E0A0BC9F3";
}

View File

@@ -56,4 +56,8 @@ public enum BusinessType
* 清空数据
*/
CLEAN,
/**
* 查询
*/
QUERY
}

View File

@@ -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;
}
}

View File

@@ -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走原始请求
}
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}
}

File diff suppressed because one or more lines are too long