更新 pom组件版本 新增安全秘钥接口 新增接口关闭 新增encryption接入方法 新增basic接入方法 新增User-Agent接入方法 支持ftp多用户 支持basic多用户 修复file接入协议错误 修复ssl伪证书错误 美化RUL报错提醒

This commit is contained in:
高雄
2025-12-25 17:04:18 +08:00
parent 92ca92bee6
commit 2dd008532a
14 changed files with 1774 additions and 769 deletions

32
pom.xml
View File

@@ -10,36 +10,36 @@
<properties>
<java.version>21</java.version>
<jodconverter.version>4.4.6</jodconverter.version>
<jodconverter.version>4.4.11</jodconverter.version>
<spring.boot.version>3.5.6</spring.boot.version>
<poi.version>5.2.2</poi.version>
<poi.version>5.2.5</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<xstream.version>1.4.20</xstream.version>
<xstream.version>1.4.21</xstream.version>
<junrar.version>7.5.5</junrar.version>
<redisson.version>3.22.0</redisson.version>
<redisson.version>4.0.0</redisson.version>
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
<jchardet.version>1.0</jchardet.version>
<antlr.version>2.7.7</antlr.version>
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
<rocksdb.version>5.17.2</rocksdb.version>
<pdfbox.version>3.0.2</pdfbox.version>
<pdfbox.version>3.0.6</pdfbox.version>
<jai-imageio.version>1.4.0</jai-imageio.version>
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
<galimatias.version>0.2.1</galimatias.version>
<bytedeco.version>1.5.2</bytedeco.version>
<opencv.version>4.1.2-1.5.2</opencv.version>
<openblas.version>0.3.6-1.5.1</openblas.version>
<ffmpeg.version>4.2.1-1.5.2</ffmpeg.version>
<itextpdf.version>5.5.13.3</itextpdf.version>
<httpclient.version>3.1</httpclient.version>
<aspose-cad.version>23.9</aspose-cad.version>
<bytedeco.version>1.5.12</bytedeco.version>
<opencv.version>4.11.0-1.5.12</opencv.version>
<openblas.version>0.3.30-1.5.12</openblas.version>
<ffmpeg.version>7.1.1-1.5.12</ffmpeg.version>
<itextpdf.version>5.5.13.4</itextpdf.version>
<httpclient.version>5.6</httpclient.version>
<aspose-cad.version>25.10</aspose-cad.version>
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
<juniversalchardet.version>1.0.3</juniversalchardet.version>
<httpcomponents.version>4.5.14</httpcomponents.version>
<httpcomponents.version>4.5.16</httpcomponents.version>
<commons-cli.version>1.5.0</commons-cli.version>
<commons-net.version>3.9.0</commons-net.version>
<commons-lang3.version>3.13.0</commons-lang3.version>
<commons-cli.version>1.11.0</commons-cli.version>
<commons-net.version>3.12.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES加密解密工具类目前AES比DES和DES3更安全速度更快对称加密一般采用AES
*/
public class AESUtil {
private static final String aesKey = ConfigConstants.getaesKey();
/**
* AES解密
*/
public static String AesDecrypt(String url) {
if (!aesKey(aesKey)) {
return null;
}
try {
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = Base64.getDecoder().decode(url);//先用base64解密
byte[] original = cipher.doFinal(encrypted1);
return new String(original, StandardCharsets.UTF_8);
} catch (Exception e) {
if (e.getMessage().contains("Given final block not properly padded. Such issues can arise if a bad key is used during decryption")) {
return "Keyerror";
}else if (e.getMessage().contains("Input byte array has incorrect ending byte")) {
return "byteerror";
}else if (e.getMessage().contains("Illegal base64 character")) {
return "base64error";
}else if (e.getMessage().contains("Input length must be multiple of 16 when decrypting with padded cipher")) {
return "byteerror";
}else {
System.out.println("ace错误:"+e);
return null;
}
}
}
/**
* AES加密
*/
public static String aesEncrypt(String url) {
if (!aesKey(aesKey)) {
return null;
}
try {
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
return new String(Base64.getEncoder().encode(encrypted));//此处使用BASE64做转码功能同时能起到2次加密的作用。
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static boolean aesKey(String aesKey) {
if (aesKey == null) {
System.out.print("Key为空null");
return false;
}
// 判断Key是否为16位
if (aesKey.length() != 16) {
System.out.print("Key长度不是16位");
return false;
}
return true;
}
}

View File

@@ -4,11 +4,10 @@ import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
@@ -22,12 +21,13 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import static cn.keking.utils.KkFileUtils.isFtpUrl;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
import static cn.keking.utils.KkFileUtils.*;
/**
* @author yudian-it
@@ -39,6 +39,7 @@ public class DownloadUtils {
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper();
@@ -53,7 +54,6 @@ public class DownloadUtils {
// 忽略ssl证书
String urlStr = null;
try {
SslUtils.ignoreSsl();
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
} catch (Exception e) {
logger.error("忽略SSL证书异常:", e);
@@ -90,17 +90,18 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) {
File realFile = new File(realPath);
factory.setConnectionRequestTimeout(2000); //设置超时时间
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000);
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
factory.setHttpClient(httpClient); //加入重定向方法
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute);
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
Map<String, String> proxyAuthorizationMap = mapper.readValue(
proxyAuthorization,
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
}
};
@@ -110,16 +111,19 @@ public class DownloadUtils {
return null;
});
} catch (Exception e) {
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败:" + e);
return response;
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败:" + e);
return response;
}
} else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
FtpUtils.download(fileAttribute.getUrl(), realPath, ftpUsername, ftpPassword, ftpControlEncoding);
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
FtpUtils.download(fileAttribute.getUrl(),ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
} else if (isFileUrl(url)) { // 添加对file协议的支持
handleFileProtocol(url, realPath);
} else {
response.setCode(1);
response.setMsg("url不能识别url" + urlStr);
@@ -138,10 +142,70 @@ public class DownloadUtils {
response.setMsg(e.getMessage());
}
return response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 处理file协议的文件下载
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
File sourceFile = new File(url.getPath());
if (!sourceFile.exists()) {
throw new FileNotFoundException("本地文件不存在: " + url.getPath());
}
if (!sourceFile.isFile()) {
throw new IOException("路径不是文件: " + url.getPath());
}
File targetFile = new File(targetPath);
// 判断源文件和目标文件是否是同一个文件(防止自身复制覆盖)
if (isSameFile(sourceFile, targetFile)) {
// 如果是同一个文件,直接返回,不执行复制操作
logger.info("源文件和目标文件相同,跳过复制: {}", sourceFile.getAbsolutePath());
return;
}
// 确保目标目录存在
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
// 复制文件
Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
* 判断两个文件是否是同一个文件
* 通过比较规范化路径来避免符号链接、相对路径等问题
*/
private static boolean isSameFile(File file1, File file2) {
try {
// 使用规范化路径比较,可以处理符号链接、相对路径等情况
String canonicalPath1 = file1.getCanonicalPath();
String canonicalPath2 = file2.getCanonicalPath();
// 如果是Windows系统忽略路径大小写
if (isWindows()) {
return canonicalPath1.equalsIgnoreCase(canonicalPath2);
}
return canonicalPath1.equals(canonicalPath2);
} catch (IOException e) {
// 如果获取规范化路径失败,使用绝对路径比较
logger.warn("无法获取文件的规范化路径,使用绝对路径比较: {}, {}", file1.getAbsolutePath(), file2.getAbsolutePath());
String absolutePath1 = file1.getAbsolutePath();
String absolutePath2 = file2.getAbsolutePath();
if (isWindows()) {
return absolutePath1.equalsIgnoreCase(absolutePath2);
}
return absolutePath1.equals(absolutePath2);
}
}
/**
* 获取真实文件绝对路径
*

View File

@@ -3,57 +3,227 @@ package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
/**
* @auther: chenjh
* @since: 2019/6/18 14:36
*/
public class FtpUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class);
public static FTPClient connect(String host, int port, String username, String password, String controlEncoding) throws IOException {
/**
* 从FTP服务器下载文件到本地
*/
public static void download(String ftpUrl, String ftpport, String localFilePath,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
// 获取FTP连接信息
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
LOGGER.debug("FTP下载 - url:{}, host:{}, port:{}, username:{}, 保存路径:{}",
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username, localFilePath);
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
connectionInfo.username, connectionInfo.password,
connectionInfo.controlEncoding);
try {
// 设置被动模式
ftpClient.enterLocalPassiveMode();
// 获取文件输入流
String encodedFilePath = new String(
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding),
StandardCharsets.ISO_8859_1
);
// 方法1直接下载文件到本地
try (OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath))) {
boolean downloadResult = ftpClient.retrieveFile(encodedFilePath, outputStream);
LOGGER.debug("FTP下载结果: {}", downloadResult);
if (!downloadResult) {
throw new IOException("FTP文件下载失败返回码: " + ftpClient.getReplyCode());
}
}
} finally {
closeFtpClient(ftpClient);
}
}
/**
* 预览FTP文件 - 返回输入流(调用者需要关闭流)
*/
public static InputStream preview(String ftpUrl, String ftpport, String localFilePath,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
// 获取FTP连接信息
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
LOGGER.debug("FTP预览 - url:{}, host:{}, port:{}, username:{}",
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username);
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
connectionInfo.username, connectionInfo.password,
connectionInfo.controlEncoding);
try {
// 设置被动模式
ftpClient.enterLocalPassiveMode();
// 获取文件输入流
String encodedFilePath = new String(
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding),
StandardCharsets.ISO_8859_1
);
// 获取文件输入流
InputStream inputStream = ftpClient.retrieveFileStream(encodedFilePath);
if (inputStream == null) {
closeFtpClient(ftpClient);
throw new IOException("无法获取FTP文件流可能文件不存在或无权限");
}
// 包装输入流在流关闭时自动断开FTP连接
return new FtpAutoCloseInputStream(inputStream, ftpClient);
} catch (IOException e) {
// 发生异常时确保关闭连接
closeFtpClient(ftpClient);
throw e;
}
}
/**
* 解析FTP连接信息抽取公共逻辑
*/
private static FtpConnectionInfo parseFtpConnectionInfo(String ftpUrl, String ftpport,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
FtpConnectionInfo info = new FtpConnectionInfo();
// 从配置获取默认连接参数
String basic = ConfigConstants.getFtpUsername();
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
String[] params = WebUtils.namePass(ftpUrl, basic);
if (params != null && params.length >= 5) {
info.port = Integer.parseInt(params[1]);
info.username = params[2];
info.password = params[3];
info.controlEncoding = params[4];
}
}
// 使用传入参数覆盖默认值
if (!StringUtils.isEmpty(ftpport)) {
info.port = Integer.parseInt(ftpport);
}
if (!StringUtils.isEmpty(ftpUsername)) {
info.username = ftpUsername;
}
if (!StringUtils.isEmpty(ftpPassword)) {
info.password = ftpPassword;
}
if (!StringUtils.isEmpty(ftpControlEncoding)) {
info.controlEncoding = ftpControlEncoding;
}
// 设置默认值
if (info.port == 0) {
info.port = 21;
}
if (StringUtils.isEmpty(info.controlEncoding)) {
info.controlEncoding = "UTF-8";
}
// 解析URL
try {
URI uri = new URI(ftpUrl);
info.host = uri.getHost();
info.remoteFilePath = uri.getPath();
} catch (URISyntaxException e) {
throw new IOException("无效的FTP URL: " + ftpUrl, e);
}
return info;
}
/**
* FTP连接信息对象
*/
private static class FtpConnectionInfo {
String host;
int port = 21;
String username;
String password;
String controlEncoding = "UTF-8";
String remoteFilePath;
}
/**
* 自动关闭FTP连接的输入流包装类
*/
private static class FtpAutoCloseInputStream extends FilterInputStream {
private final FTPClient ftpClient;
protected FtpAutoCloseInputStream(InputStream in, FTPClient ftpClient) {
super(in);
this.ftpClient = ftpClient;
}
@Override
public void close() throws IOException {
try {
super.close();
// 确保FTP命令完成
if (ftpClient != null) {
ftpClient.completePendingCommand();
}
} finally {
closeFtpClient(ftpClient);
}
}
}
/**
* 安全关闭FTP连接
*/
private static void closeFtpClient(FTPClient ftpClient) {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
LOGGER.warn("关闭FTP连接时发生异常", e);
}
}
}
/**
* 连接FTP服务器
*/
private static FTPClient connect(String host, int port, String username,
String password, String controlEncoding) throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.connect(host, port);
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
ftpClient.login(username, password);
}
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect();
}
ftpClient.setControlEncoding(controlEncoding);
ftpClient.connect(host, port);
if (!ftpClient.login(username, password)) {
throw new IOException("FTP登录失败用户名或密码错误");
}
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
return ftpClient;
}
public static void download(String ftpUrl, String localFilePath, String ftpUsername, String ftpPassword, String ftpControlEncoding) throws IOException {
String username = StringUtils.isEmpty(ftpUsername) ? ConfigConstants.getFtpUsername() : ftpUsername;
String password = StringUtils.isEmpty(ftpPassword) ? ConfigConstants.getFtpPassword() : ftpPassword;
String controlEncoding = StringUtils.isEmpty(ftpControlEncoding) ? ConfigConstants.getFtpControlEncoding() : ftpControlEncoding;
URL url = new URL(ftpUrl);
String host = url.getHost();
int port = (url.getPort() == -1) ? url.getDefaultPort() : url.getPort();
String remoteFilePath = url.getPath();
LOGGER.debug("FTP connection url:{}, username:{}, password:{}, controlEncoding:{}, localFilePath:{}", ftpUrl, username, password, controlEncoding, localFilePath);
FTPClient ftpClient = connect(host, port, username, password, controlEncoding);
OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath));
ftpClient.enterLocalPassiveMode();
boolean downloadResult = ftpClient.retrieveFile(new String(remoteFilePath.getBytes(controlEncoding), StandardCharsets.ISO_8859_1), outputStream);
LOGGER.debug("FTP download result {}", downloadResult);
outputStream.flush();
outputStream.close();
ftpClient.logout();
ftpClient.disconnect();
}
}

View File

@@ -9,9 +9,8 @@ import org.springframework.web.util.HtmlUtils;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.regex.Pattern;
public class KkFileUtils {
@@ -19,17 +18,31 @@ public class KkFileUtils {
public static final String DEFAULT_FILE_ENCODING = "UTF-8";
private static final List<String> illegalFileStrList = new ArrayList<>();
// 路径遍历关键字列表
private static final Set<String> illegalFileStrList;
static {
illegalFileStrList.add("../");
illegalFileStrList.add("./");
illegalFileStrList.add("..\\");
illegalFileStrList.add(".\\");
illegalFileStrList.add("\\..");
illegalFileStrList.add("\\.");
illegalFileStrList.add("..");
illegalFileStrList.add("...");
Set<String> set = new HashSet<>();
// 基本路径遍历
Collections.addAll(set, "../", "./", "..\\", ".\\", "\\..", "\\.", "..", "...", "....", ".....");
// URL编码的路径遍历
Collections.addAll(set, "%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "%2e%2e\\", "..%5c",
"%252e%252e%252f", "%252e%252e/", "..%252f");
// Unicode编码绕过
Collections.addAll(set, "\\u002e\\u002e\\u002f", "\\U002e\\U002e\\U002f",
"\u00c0\u00ae\u00c0\u00ae", "\u00c1\u009c\u00c1\u009c");
// 特殊分隔符
Collections.addAll(set, "|..|", "|../|", "|..\\|");
// Windows特殊路径
Collections.addAll(set, "\\\\?\\", "\\\\.\\");
// 转换为不可变集合
illegalFileStrList = Collections.unmodifiableSet(set);
}
/**
@@ -68,7 +81,22 @@ public class KkFileUtils {
* @return 是否http
*/
public static boolean isHttpUrl(URL url) {
return url.getProtocol().toLowerCase().startsWith("file") || url.getProtocol().toLowerCase().startsWith("http");
return url.getProtocol().toLowerCase().startsWith("http") || url.getProtocol().toLowerCase().startsWith("https");
}
/**
* 判断url是否是file资源
*
*/
public static boolean isFileUrl(URL url) {
return url.getProtocol().toLowerCase().startsWith("file");
}
/**
* 判断当前操作系统是否为Windows
*/
static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
/**

View File

@@ -1,42 +1,90 @@
package cn.keking.utils;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* @author 鞠玉果
* @author 高雄
*/
public class SslUtils {
private static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[1];
TrustManager tm = new miTM();
trustAllCerts[0] = tm;
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
/**
* 创建忽略SSL验证的HttpClient适用于HttpClient 5.6
*/
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
// 创建自定义的SSL上下文
SSLContext sslContext = createIgnoreVerifySSL();
static class miTM implements TrustManager, X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext, NoopHostnameVerifier.INSTANCE);
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
// 使用新的PoolingHttpClientConnectionManagerBuilder构建连接管理器
// 使用连接管理器构建器
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
// 配置连接池参数
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
// 配置请求参数
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(72))
.setConnectionRequestTimeout(Timeout.ofSeconds(2))
.setRedirectsEnabled(true)
.setMaxRedirects(5)
.build();
return HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
.build();
}
/**
* 忽略HTTPS请求的SSL证书必须在openConnection之前调用
* 创建忽略SSL验证的SSLContext
*/
public static void ignoreSsl() throws Exception {
HostnameVerifier hv = (urlHostName, session) -> true;
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier(hv);
}
private static SSLContext createIgnoreVerifySSL() throws Exception {
// 使用TLSv1.2或TLSv1.3
SSLContext sc = SSLContext.getInstance("TLSv1.2");
// 实现一个X509TrustManager忽略所有证书验证
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// 信任所有客户端证书
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// 信任所有服务器证书
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sc.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
return sc;
}
}

View File

@@ -1,10 +1,14 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;
@@ -17,8 +21,7 @@ import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,6 +33,18 @@ public class WebUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
private static final String BASE64_MSG = "base64";
private static final String URL_PARAM_BASIC_NAME = "basic.name";
private static final String URL_PARAM_BASIC_PASS = "basic.pass";
private static final Map<String, String> ERROR_MESSAGES = Map.of(
"base64", "KK提醒您:接入方法错误未使用BASE64",
"base641", "KK提醒您:BASE64解码异常,确认是否正确使用BASE64编码",
"Keyerror", "KK提醒您:AES解码错误请检测你的秘钥是否正确",
"base64error", "KK提醒您:你选用的是ASE加密实际用了BASE64加密接入",
"byteerror", "KK提醒您:解码异常,检测你接入方法是否正确"
);
private static final String EMPTY_URL_MSG = "KK提醒您:地址不能为空";
private static final String INVALID_URL_MSG = "KK提醒您:请正确使用URL(必须包括https ftp file 协议)";
/**
* 获取标准的URL
*
@@ -234,17 +249,18 @@ public class WebUtils {
String urls = request.getParameter("urls");
String currentUrl = request.getParameter("currentUrl");
String urlPath = request.getParameter("urlPath");
String encryption = request.getParameter("encryption");
if (StringUtils.isNotBlank(url)) {
return decodeUrl(url);
return decodeUrl(url,encryption);
}
if (StringUtils.isNotBlank(currentUrl)) {
return decodeUrl(currentUrl);
return decodeUrl(currentUrl,encryption);
}
if (StringUtils.isNotBlank(urlPath)) {
return decodeUrl(urlPath);
return decodeUrl(urlPath,encryption);
}
if (StringUtils.isNotBlank(urls)) {
urls = decodeUrl(urls);
urls = decodeUrl(urls,encryption);
String[] images = urls.split("\\|");
return images[0];
}
@@ -268,13 +284,20 @@ public class WebUtils {
*
* aHR0cHM6Ly9maWxlLmtla2luZy5jbi9kZW1vL%2BS4reaWhy5wcHR4 -> https://file.keking.cn/demo/%E4%B8%AD%E6%96%87.pptx -> https://file.keking.cn/demo/中文.pptx
*/
public static String decodeUrl(String source) {
String url = decodeBase64String(source, StandardCharsets.UTF_8);
if (! StringUtils.isNotBlank(url)){
return null;
public static String decodeUrl(String source,String encryption) {
String url;
if(ObjectUtils.isEmpty(encryption) || Objects.equals(ConfigConstants.getaesKey(), "false")){
encryption = "base64";
}
if(Objects.equals(encryption.toLowerCase(), "aes")){
return AESUtil.AesDecrypt(source);
}else {
url = decodeBase64String(source, StandardCharsets.UTF_8);
if(!isValidUrl(url)){
url="base641";
}
return url;
}
return url;
}
/**
@@ -301,6 +324,30 @@ public class WebUtils {
}
}
public static String urlSecurity(String url) {
if (ObjectUtils.isEmpty(url)) {
return EMPTY_URL_MSG;
}
// 检查已知的错误类型
String errorMsg = ERROR_MESSAGES.get(url);
if (errorMsg != null) {
return errorMsg;
}
// 验证URL格式
if (!isValidUrl(url)) {
return INVALID_URL_MSG;
}
// file协议特殊处理
if (url.toLowerCase().startsWith("file://")) {
// 对于本地文件可以返回URL本身或进行特殊处理
// 根据业务需求决定返回URL、返回特殊标识或进行本地文件安全检查
return url; // 或者返回特殊标识如 "file-protocol"
}
// 提取主机名
return getHost(url);
}
/**
* 获取 url 的 host
* @param urlStr url
@@ -371,4 +418,93 @@ public class WebUtils {
}
session.removeAttribute(key);
}
public static boolean validateKey(String key) {
String configKey = ConfigConstants.getKey();
return !"false".equals(configKey) && !configKey.equals(key);
}
public static String getContentTypeByFilename(String filename) {
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
switch (extension) {
case "pdf": return "application/pdf";
case "jpg": case "jpeg": return "image/jpeg";
case "png": return "image/png";
case "gif": return "image/gif";
case "svg": return "image/svg+xml";
case "txt": return "text/plain";
case "html": case "htm": return "text/html";
case "xml": return "application/xml";
case "json": return "application/json";
default: return null;
}
}
/**
* name pass 获取用户名 和密码
*/
public static String[] namePass(String url,String name) {
url= getHost(url);
String[] items = name.split(",\\s*");
String toRemove = ":";
String names = null;
String[] parts = null;
try {
for (String item : items) {
int index = item.indexOf(toRemove);
if (index != -1) {
String result = item.substring(0, index);
if (Objects.equals(result, url)) {
names = item;
}
}
}
if (names !=null){
parts = names.split(toRemove);
}
} catch (Exception e) {
LOGGER.error("获取认证权限错误:",e);
}
return parts;
}
/**
* 支持basic 下载方法
*/
public static void applyBasicAuthHeaders(HttpHeaders headers, FileAttribute fileAttribute) {
String url = fileAttribute.getUrl();
// 从配置文件读取User-Agent如果没有配置则使用默认值
String customUserAgent=ConfigConstants.getUserAgent();
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
if (!StringUtils.isEmpty(customUserAgent) && !Objects.equals(customUserAgent, "false")) {
userAgent = customUserAgent;
}
headers.set("User-Agent", userAgent);
// 获取用户名和密码
String username = null;
String password = null;
// 从basic配置获取
String basic = ConfigConstants.getBasicName();
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
String[] urlUser = namePass(url, basic);
if (urlUser != null && urlUser.length >= 3) {
username = urlUser[1];
password = urlUser[2];
}
}
// URL参数优先
String basicUsername = getUrlParameterReg(url, URL_PARAM_BASIC_NAME);
String basicPassword = getUrlParameterReg(url, URL_PARAM_BASIC_PASS);
if (!StringUtils.isEmpty(basicUsername)) {
username = basicUsername;
password = basicPassword;
}
// 设置Basic Auth
if (!StringUtils.isEmpty(username)) {
String plainCredentials = username + ":" + (password != null ? password : "");
String base64Credentials = java.util.Base64.getEncoder().encodeToString(plainCredentials.getBytes());
headers.set("Authorization", "Basic " + base64Credentials);
}
}
}

View File

@@ -179,7 +179,7 @@ public class FileController {
return ReturnResponse.failure("文件名为空,删除失败!");
}
try {
fileName = WebUtils.decodeUrl(fileName);
fileName = WebUtils.decodeUrl(fileName,"base64");
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName);
return ReturnResponse.failure(errorMsg + "删除失败!");
@@ -208,7 +208,7 @@ public class FileController {
public Object directory(String urls) {
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(urls);
fileUrl = WebUtils.decodeUrl(urls,"base64");
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return ReturnResponse.failure(errorMsg);

View File

@@ -1,18 +1,24 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.FtpUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.SslUtils;
import cn.keking.utils.WebUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,6 +30,7 @@ import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
@@ -38,6 +45,8 @@ import java.util.List;
import java.util.Map;
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
import static cn.keking.utils.KkFileUtils.isFtpUrl;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
/**
* @author yudian-it
@@ -45,8 +54,14 @@ import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
@Controller
public class OnlinePreviewController {
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private final Logger logger = LoggerFactory.getLogger(OnlinePreviewController.class);
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private static final String ILLEGAL_ACCESS_MSG = "访问不合法:访问密码不正确";
private static final String INTERFACE_CLOSED_MSG = "接口关闭,禁止访问!";
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private final FilePreviewFactory previewFactory;
private final CacheService cacheService;
@@ -64,11 +79,18 @@ public class OnlinePreviewController {
}
@GetMapping( "/onlinePreview")
public String onlinePreview(String url, Model model, HttpServletRequest req) {
public String onlinePreview(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
Model model,
HttpServletRequest req) {
// 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(url);
fileUrl = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return otherFilePreview.notSupportedFile(model, errorMsg);
@@ -85,10 +107,22 @@ public class OnlinePreviewController {
}
@GetMapping( "/picturesPreview")
public String picturesPreview(String urls, Model model, HttpServletRequest req) {
public String picturesPreview(@RequestParam String urls,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
Model model,
HttpServletRequest req) {
// 1. 验证接口是否开启
if (!ConfigConstants.getPicturesPreview()) {
return otherFilePreview.notSupportedFile(model, INTERFACE_CLOSED_MSG);
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(urls);
fileUrls = WebUtils.decodeUrl(urls, encryption);
// 防止XSS攻击
fileUrls = KkFileUtils.htmlEscape(fileUrls);
} catch (Exception ex) {
@@ -103,7 +137,7 @@ public class OnlinePreviewController {
String currentUrl = req.getParameter("currentUrl");
if (StringUtils.hasText(currentUrl)) {
String decodedCurrentUrl = new String(Base64.decodeBase64(currentUrl));
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
model.addAttribute("currentUrl", decodedCurrentUrl);
} else {
model.addAttribute("currentUrl", imgUrls.get(0));
@@ -119,35 +153,50 @@ public class OnlinePreviewController {
* @param response response
*/
@GetMapping("/getCorsFile")
public void getCorsFile(String urlPath, HttpServletResponse response,FileAttribute fileAttribute) throws IOException {
public void getCorsFile(@RequestParam String urlPath,
@RequestParam(required = false) String key,
HttpServletResponse response,
FileAttribute fileAttribute) throws Exception {
// 1. 验证接口是否开启
if (!ConfigConstants.getGetCorsFile()) {
logger.info("接口关闭,禁止访问!url{}", urlPath);
return;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
logger.info("访问不合法:访问密码不正确!url{}", urlPath);
return;
}
URL url;
try {
urlPath = WebUtils.decodeUrl(urlPath);
urlPath = WebUtils.decodeUrl(urlPath, "base64");
url = WebUtils.normalizedURL(urlPath);
} catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex);
return;
}
assert urlPath != null;
if (!urlPath.toLowerCase().startsWith("http") && !urlPath.toLowerCase().startsWith("https") && !urlPath.toLowerCase().startsWith("ftp")) {
if (!isHttpUrl(url) && !isFtpUrl(url)) {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return;
}
InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath);
if (!urlPath.toLowerCase().startsWith("ftp:")) {
factory.setConnectionRequestTimeout(2000);
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000);
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
if (!isFtpUrl(url)) {
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
factory.setHttpClient(httpClient);
// restTemplate.setRequestFactory(factory);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
Map<String, String> proxyAuthorizationMap = mapper.readValue(
proxyAuthorization,
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
);
proxyAuthorizationMap.forEach((headerKey, value) -> request.getHeaders().set(headerKey, value));
}
};
try {
@@ -160,10 +209,16 @@ public class OnlinePreviewController {
}
}else{
try {
if(urlPath.contains(".svg")) {
response.setContentType("image/svg+xml");
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
String contentType = WebUtils.getContentTypeByFilename(filename);
if (contentType != null) {
response.setContentType(contentType);
}
inputStream = (url).openStream();
String ftpUsername = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING);
String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT);
inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
IOUtils.copy(inputStream, response.getOutputStream());
} catch (IOException e) {
logger.error("读取跨域文件异常url{}", urlPath);
@@ -180,9 +235,32 @@ public class OnlinePreviewController {
*/
@GetMapping("/addTask")
@ResponseBody
public String addQueueTask(String url) {
logger.info("添加转码队列url{}", url);
cacheService.addQueueTask(url);
public String addQueueTask(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption) {
// 1. 验证接口是否开启
if (!ConfigConstants.getAddTask()) {
String errorMsg = "接口关闭,禁止访问!";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
String errorMsg = "Url解析错误";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
String errorMsg = "访问不合法:访问密码不正确!";
logger.info("{}url{}", errorMsg, fileUrls);
return errorMsg;
}
logger.info("添加转码队列url{}", fileUrls);
cacheService.addQueueTask(fileUrls);
return "success";
}
}

View File

@@ -1,6 +1,8 @@
package cn.keking.web.filter;
import cn.keking.config.ConfigConstants;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import io.mola.galimatias.GalimatiasParseException;
import org.jodconverter.core.util.OSUtils;
@@ -11,6 +13,8 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
@@ -56,18 +60,42 @@ public class TrustDirFilter implements Filter {
}
private boolean allowPreview(String urlPath) {
//判断URL是否合法
if(!StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false ;
// 判断URL是否合法
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false;
}
try {
URL url = WebUtils.normalizedURL(urlPath);
if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) {
String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
if (OSUtils.IS_OS_WINDOWS) {
filePath = filePath.replaceAll("/", "\\\\");
// 将文件路径转换为File对象
File targetFile = new File(filePath);
// 将配置目录也转换为File对象
File fileDir = new File(ConfigConstants.getFileDir());
File localPreviewDir = new File(ConfigConstants.getLocalPreviewDir());
try {
// 获取规范路径(系统会自动处理大小写、符号链接、相对路径等)
String canonicalFilePath = targetFile.getCanonicalPath();
String canonicalFileDir = fileDir.getCanonicalPath();
String canonicalLocalPreviewDir = localPreviewDir.getCanonicalPath();
// 检查文件是否在配置目录下
return isSubDirectory(canonicalFileDir, canonicalFilePath) || isSubDirectory(canonicalLocalPreviewDir, canonicalFilePath);
} catch (IOException e) {
logger.warn("获取规范路径失败,使用原始路径比较", e);
// 如果获取规范路径失败,回退到原始路径比较
String absFilePath = targetFile.getAbsolutePath();
String absFileDir = fileDir.getAbsolutePath();
String absLocalPreviewDir = localPreviewDir.getAbsolutePath();
// 统一路径分隔符
absFilePath = absFilePath.replace('\\', '/');
absFileDir = absFileDir.replace('\\', '/');
absLocalPreviewDir = absLocalPreviewDir.replace('\\', '/');
// 确保目录以斜杠结尾
if (!absFileDir.endsWith("/")) absFileDir += "/";
if (!absLocalPreviewDir.endsWith("/")) absLocalPreviewDir += "/";
return absFilePath.startsWith(absFileDir) || absFilePath.startsWith(absLocalPreviewDir);
}
return filePath.startsWith(ConfigConstants.getFileDir()) || filePath.startsWith(ConfigConstants.getLocalPreviewDir());
}
return true;
} catch (IOException | GalimatiasParseException e) {
@@ -75,4 +103,26 @@ public class TrustDirFilter implements Filter {
return false;
}
}
/**
* 检查子路径是否在父路径下(跨平台)
*/
private boolean isSubDirectory(String parentDir, String childPath) {
try {
File parent = new File(parentDir);
File child = new File(childPath);
// 获取规范路径
String canonicalParent = parent.getCanonicalPath();
String canonicalChild = child.getCanonicalPath();
// 确保父目录以路径分隔符结尾
if (!canonicalParent.endsWith(File.separator)) {
canonicalParent += File.separator;
}
// 比较路径
return canonicalChild.startsWith(canonicalParent);
} catch (IOException e) {
logger.warn("检查子路径失败", e);
return false;
}
}
}

View File

@@ -42,9 +42,9 @@ public class TrustHostFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = WebUtils.getSourceUrl(request);
String host = WebUtils.getHost(url);
String host = WebUtils.urlSecurity(url); //启用地址检查
assert host != null;
if (isNotTrustHost(host)) {
if (isNotTrustHost(host)||!WebUtils.isValidUrl(url)) {
String html = this.notTrustHostHtmlView.replace("${current_host}", host);
response.getWriter().write(html);
response.getWriter().close();
@@ -58,7 +58,6 @@ public class TrustHostFilter implements Filter {
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) {
return ConfigConstants.getNotTrustHostSet().contains(host);
}
// 如果配置了白名单,检查是否在白名单中
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
// 支持通配符 * 表示允许所有主机

View File

@@ -33,6 +33,36 @@
<div class="page-header">
<h1>版本发布记录</h1>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">2025年12月25日v5.0版本</h3>
</div>
<div class="panel-body">
<div>
<h4>优化</h4>
1. 优化 OFD 移动端预览 页面不自适应 <br>
2. 更新 xlsx 前端解析组件,加速解析速度 <br>
3. 升级 CAD 组件 <br>
4. office 功能调整,支持批注、转换页码限制、生成水印等功能 <br>
5. 升级 markdown 组件 <br>
6. 升级 dcm 解析组件 <br>
7. 升级 PDF.JS 解析组件 <br>
8. 更换视频播放插件为 ckplayer <br>
9. tif 解析更加智能化,支持被修改的图片格式 <br>
10. 针对大小文本文件检测字符编码的正确率,处理并发隐患 <br>
11. 重构下载文件的代码,新增通用的文件服务器认证访问的设计 <br>
12. 更新 bootstrap 组件,并精简掉不需要的文件 <br>
13. 更新 epub 版本,优化 epub 显示效果 <br>
14. 解决定时清除缓存时,对于多媒体类型文件,只删除了磁盘缓存文件 <br>
15. 自动检测已安装 Office 组件,增加 LibreOffice 7.5 & 7.6 版本默认路径 <br>
16. 修改 drawio 默认为预览模式 <br>
17. 新增 PDF 线程管理、超时管理、内存缓存管理,更新 PDF 解析组件版本 <br>
18. 优化 Dockerfile支持真正的跨平台构建镜像 <br>
<br>
</div>
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">2025年01月16日v4.4.0版本</h3>