mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-11-11 13:50:44 +00:00
600 lines
21 KiB
Java
600 lines
21 KiB
Java
package cn.keking.utils;
|
||
|
||
import cn.keking.config.ConfigConstants;
|
||
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.http.MediaType;
|
||
import org.springframework.http.client.ClientHttpResponse;
|
||
import org.springframework.util.ObjectUtils;
|
||
import org.springframework.web.multipart.MultipartFile;
|
||
import org.springframework.web.util.HtmlUtils;
|
||
|
||
import jakarta.servlet.ServletRequest;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import jakarta.servlet.http.HttpSession;
|
||
import java.io.UnsupportedEncodingException;
|
||
import java.net.MalformedURLException;
|
||
import java.net.URL;
|
||
import java.net.URLEncoder;
|
||
import java.nio.charset.Charset;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.*;
|
||
import java.util.regex.Matcher;
|
||
import java.util.regex.Pattern;
|
||
|
||
/**
|
||
* @author : kl
|
||
* create : 2020-12-27 1:30 上午
|
||
**/
|
||
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
|
||
*
|
||
* @param urlStr url
|
||
* @return 标准的URL
|
||
*/
|
||
public static URL normalizedURL(String urlStr) throws GalimatiasParseException, MalformedURLException {
|
||
return io.mola.galimatias.URL.parse(urlStr).toJavaURL();
|
||
}
|
||
|
||
|
||
/**
|
||
* 对文件名进行编码
|
||
*
|
||
*/
|
||
public static String encodeFileName(String name) {
|
||
name = URLEncoder.encode(name, StandardCharsets.UTF_8)
|
||
.replaceAll("%2F", "/") // 恢复斜杠
|
||
.replaceAll("%5C", "/") // 恢复反斜杠
|
||
.replaceAll("\\+", "%20"); // 空格处理
|
||
return name;
|
||
}
|
||
|
||
|
||
/**
|
||
* 去除fullfilename参数
|
||
*
|
||
* @param urlStr
|
||
* @return
|
||
*/
|
||
public static String clearFullfilenameParam(String urlStr) {
|
||
// 去除特定参数字段
|
||
Pattern pattern = Pattern.compile("(&fullfilename=[^&]*)");
|
||
Matcher matcher = pattern.matcher(urlStr);
|
||
return matcher.replaceAll("");
|
||
}
|
||
|
||
/**
|
||
* 对URL进行编码
|
||
*/
|
||
public static String urlEncoderencode(String urlStr) {
|
||
|
||
String fullFileName = getUrlParameterReg(urlStr, "fullfilename"); //获取流文件名
|
||
if (org.springframework.util.StringUtils.hasText(fullFileName)) {
|
||
// 移除fullfilename参数
|
||
urlStr = clearFullfilenameParam(urlStr);
|
||
} else {
|
||
fullFileName = getFileNameFromURL(urlStr); //获取文件名
|
||
}
|
||
if (KkFileUtils.isIllegalFileName(fullFileName)) { //判断文件名是否带有穿越漏洞
|
||
return null;
|
||
}
|
||
if (!UrlEncoderUtils.hasUrlEncoded(fullFileName)) { //判断文件名是否转义
|
||
try {
|
||
urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "=");
|
||
} catch (UnsupportedEncodingException e) {
|
||
LOGGER.error("Failed to encode URL: {}", urlStr, e);
|
||
}
|
||
}
|
||
return urlStr;
|
||
}
|
||
|
||
/**
|
||
* 获取url中的参数
|
||
*
|
||
* @param url url
|
||
* @param name 参数名
|
||
* @return 参数值
|
||
*/
|
||
public static String getUrlParameterReg(String url, String name) {
|
||
Map<String, String> mapRequest = new HashMap<>();
|
||
String strUrlParam = truncateUrlPage(url);
|
||
if (strUrlParam == null) {
|
||
return "";
|
||
}
|
||
//每个键值为一组
|
||
String[] arrSplit = strUrlParam.split("[&]");
|
||
for (String strSplit : arrSplit) {
|
||
String[] arrSplitEqual = strSplit.split("[=]");
|
||
//解析出键值
|
||
if (arrSplitEqual.length > 1) {
|
||
//正确解析
|
||
mapRequest.put(arrSplitEqual[0], arrSplitEqual[1]);
|
||
} else if (!arrSplitEqual[0].equals("")) {
|
||
//只有参数没有值,不加入
|
||
mapRequest.put(arrSplitEqual[0], "");
|
||
}
|
||
}
|
||
return mapRequest.get(name);
|
||
}
|
||
|
||
|
||
/**
|
||
* 去掉url中的路径,留下请求参数部分
|
||
*
|
||
* @param strURL url地址
|
||
* @return url请求参数部分
|
||
*/
|
||
private static String truncateUrlPage(String strURL) {
|
||
String strAllParam = null;
|
||
strURL = strURL.trim();
|
||
String[] arrSplit = strURL.split("[?]");
|
||
if (strURL.length() > 1) {
|
||
if (arrSplit.length > 1) {
|
||
if (arrSplit[1] != null) {
|
||
strAllParam = arrSplit[1];
|
||
}
|
||
}
|
||
}
|
||
return strAllParam;
|
||
}
|
||
|
||
/**
|
||
* 从url中剥离出文件名
|
||
*
|
||
* @param url 格式如:http://www.com.cn/20171113164107_月度绩效表模板(新).xls?UCloudPublicKey=ucloudtangshd@weifenf.com14355492830001993909323&Expires=&Signature=I D1NOFtAJSPT16E6imv6JWuq0k=
|
||
* @return 文件名
|
||
*/
|
||
public static String getFileNameFromURL(String url) {
|
||
if (url.toLowerCase().startsWith("file:")) {
|
||
try {
|
||
URL urlObj = new URL(url);
|
||
url = urlObj.getPath().substring(1);
|
||
} catch (MalformedURLException e) {
|
||
LOGGER.error("Failed to parse file URL: {}", url, e);
|
||
}
|
||
}
|
||
// 因为url的参数中可能会存在/的情况,所以直接url.lastIndexOf("/")会有问题
|
||
// 所以先从?处将url截断,然后运用url.lastIndexOf("/")获取文件名
|
||
String noQueryUrl = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
|
||
return noQueryUrl.substring(noQueryUrl.lastIndexOf("/") + 1);
|
||
}
|
||
|
||
/**
|
||
* 从url中剥离出文件名
|
||
* @param file 文件
|
||
* @return 文件名
|
||
*/
|
||
public static String getFileNameFromMultipartFile(MultipartFile file) {
|
||
String fileName = file.getOriginalFilename();
|
||
//判断是否为IE浏览器的文件名,IE浏览器下文件名会带有盘符信
|
||
// escaping dangerous characters to prevent XSS
|
||
assert fileName != null;
|
||
fileName = HtmlUtils.htmlEscape(fileName, KkFileUtils.DEFAULT_FILE_ENCODING);
|
||
|
||
// Check for Unix-style path
|
||
int unixSep = fileName.lastIndexOf('/');
|
||
// Check for Windows-style path
|
||
int winSep = fileName.lastIndexOf('\\');
|
||
// Cut off at latest possible point
|
||
int pos = (Math.max(winSep, unixSep));
|
||
if (pos != -1) {
|
||
fileName = fileName.substring(pos + 1);
|
||
}
|
||
return fileName;
|
||
}
|
||
|
||
|
||
/**
|
||
* 从url中获取文件后缀
|
||
*
|
||
* @param url url
|
||
* @return 文件后缀
|
||
*/
|
||
public static String suffixFromUrl(String url) {
|
||
String nonPramStr = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
|
||
String fileName = nonPramStr.substring(nonPramStr.lastIndexOf("/") + 1);
|
||
return KkFileUtils.suffixFromFileName(fileName);
|
||
}
|
||
|
||
/**
|
||
* 对url中的文件名进行UTF-8编码
|
||
*
|
||
* @param url url
|
||
* @return 文件名编码后的url
|
||
*/
|
||
public static String encodeUrlFileName(String url) {
|
||
String encodedFileName;
|
||
String noQueryUrl = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
|
||
int fileNameStartIndex = noQueryUrl.lastIndexOf('/') + 1;
|
||
int fileNameEndIndex = noQueryUrl.lastIndexOf('.');
|
||
if (fileNameEndIndex < fileNameStartIndex) {
|
||
return url;
|
||
}
|
||
encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), StandardCharsets.UTF_8);
|
||
return url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex);
|
||
}
|
||
|
||
/**
|
||
* 从 ServletRequest 获取预览的源 url , 已 base64 解码
|
||
*
|
||
* @param request 请求 request
|
||
* @return url
|
||
*/
|
||
public static String getSourceUrl(ServletRequest request) {
|
||
List<String> urls = getSourceUrls(request);
|
||
return urls.isEmpty() ? null : urls.get(0);
|
||
}
|
||
|
||
/** Validate every candidate consumed by preview endpoints, including all images. */
|
||
public static List<String> getSourceUrls(ServletRequest request) {
|
||
List<String> urls = new ArrayList<>();
|
||
String encryption = request.getParameter("encryption");
|
||
try {
|
||
for (String name : List.of("url", "currentUrl", "urlPath", "urls")) {
|
||
String[] values = request.getParameterValues(name);
|
||
if (values == null) {
|
||
continue;
|
||
}
|
||
for (String value : values) {
|
||
if (StringUtils.isBlank(value)) {
|
||
continue;
|
||
}
|
||
// picturesPreview decodes currentUrl as Base64 independently of encryption.
|
||
String decoded = "currentUrl".equals(name)
|
||
? decodeBase64String(value, StandardCharsets.UTF_8) : decodeUrl(value, encryption);
|
||
if (decoded == null) {
|
||
return List.of("");
|
||
}
|
||
if ("urls".equals(name)) {
|
||
urls.addAll(Arrays.asList(decoded.split("\\|", -1)));
|
||
} else {
|
||
urls.add(decoded);
|
||
}
|
||
}
|
||
}
|
||
} catch (RuntimeException e) {
|
||
return List.of("");
|
||
}
|
||
return urls;
|
||
}
|
||
/**
|
||
* 判断地址是否正确
|
||
* 高 2022/12/17
|
||
*/
|
||
public static boolean isValidUrl(String url) {
|
||
if (url == null) {
|
||
return false;
|
||
}
|
||
String regStr = "^((https|http|ftp|rtsp|mms|file)://)";//[.?*]表示匹配的就是本身
|
||
Pattern pattern = Pattern.compile(regStr);
|
||
Matcher matcher = pattern.matcher(url);
|
||
return matcher.find();
|
||
}
|
||
|
||
/**
|
||
* 将 Base64 字符串解码,再解码URL参数, 默认使用 UTF-8
|
||
* @param source 原始 Base64 字符串
|
||
* @return decoded string
|
||
*
|
||
* 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 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将 Base64 字符串使用指定字符集解码
|
||
* @param source 原始 Base64 字符串
|
||
* @param charsets 字符集
|
||
* @return decoded string
|
||
*/
|
||
public static String decodeBase64String(String source, Charset charsets) {
|
||
/*
|
||
* url 传入的参数里加号会被替换成空格,导致解析出错,这里需要把空格替换回加号
|
||
* 有些 Base64 实现可能每 76 个字符插入换行符,也一并去掉
|
||
* https://github.com/kekingcn/kkFileView/pull/340
|
||
*/
|
||
try {
|
||
return new String(Base64.decodeBase64(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
|
||
} catch (Exception e) {
|
||
if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
|
||
LOGGER.error("url解码异常,接入方法错误未使用BASE64");
|
||
}else {
|
||
LOGGER.error("url解码异常,其他错误", e);
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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
|
||
* @return host
|
||
*/
|
||
public static String getHost(String urlStr) {
|
||
if (urlStr == null) {
|
||
return null;
|
||
}
|
||
try {
|
||
URL url = normalizedURL(urlStr);
|
||
return url.getHost().toLowerCase(Locale.ROOT);
|
||
} catch (GalimatiasParseException | MalformedURLException | IllegalArgumentException ignored) {
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取 session 中的 String 属性
|
||
* @param request 请求
|
||
* @return 属性值
|
||
*/
|
||
public static String getSessionAttr(HttpServletRequest request, String key) {
|
||
HttpSession session = request.getSession();
|
||
if (session == null) {
|
||
return null;
|
||
}
|
||
Object value = session.getAttribute(key);
|
||
if (value == null) {
|
||
return null;
|
||
}
|
||
return value.toString();
|
||
}
|
||
|
||
/**
|
||
* 获取 session 中的 long 属性
|
||
* @param request 请求
|
||
* @param key 属性名
|
||
* @return 属性值
|
||
*/
|
||
public static long getLongSessionAttr(HttpServletRequest request, String key) {
|
||
String value = getSessionAttr(request, key);
|
||
if (value == null) {
|
||
return 0;
|
||
}
|
||
return Long.parseLong(value);
|
||
}
|
||
|
||
/**
|
||
* session 中设置属性
|
||
* @param request 请求
|
||
* @param key 属性名
|
||
*/
|
||
public static void setSessionAttr(HttpServletRequest request, String key, Object value) {
|
||
HttpSession session = request.getSession();
|
||
if (session == null) {
|
||
return;
|
||
}
|
||
session.setAttribute(key, value);
|
||
}
|
||
|
||
/**
|
||
* 移除 session 中的属性
|
||
* @param request 请求
|
||
* @param key 属性名
|
||
*/
|
||
public static void removeSessionAttr(HttpServletRequest request, String key) {
|
||
HttpSession session = request.getSession();
|
||
if (session == null) {
|
||
return;
|
||
}
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 获取Content-Type
|
||
*/
|
||
|
||
public static String headersType(ClientHttpResponse fileResponse) {
|
||
if (fileResponse == null) {
|
||
return null;
|
||
}
|
||
HttpHeaders headers = fileResponse.getHeaders();
|
||
if (headers == null) {
|
||
return null;
|
||
}
|
||
String contentTypeStr = null;
|
||
try {
|
||
// 直接获取Content-Type头字符串
|
||
contentTypeStr = headers.getFirst(HttpHeaders.CONTENT_TYPE);
|
||
if (contentTypeStr == null || contentTypeStr.isEmpty()) {
|
||
return null;
|
||
}
|
||
// 解析为MediaType对象
|
||
MediaType mediaType = MediaType.parseMediaType(contentTypeStr);
|
||
// 返回主类型和子类型,忽略参数
|
||
return mediaType.getType() + "/" + mediaType.getSubtype();
|
||
} catch (Exception e) {
|
||
// 如果解析失败,尝试简单的字符串处理
|
||
if (contentTypeStr != null) {
|
||
// 移除分号及后面的参数
|
||
int semicolonIndex = contentTypeStr.indexOf(';');
|
||
if (semicolonIndex > 0) {
|
||
return contentTypeStr.substring(0, semicolonIndex).trim();
|
||
}
|
||
return contentTypeStr.trim();
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断文件是否需要校验MIME类型
|
||
* @param suffix 文件后缀
|
||
* @return 是否需要校验
|
||
*/
|
||
public static boolean isMimeCheckRequired(String suffix) {
|
||
if (suffix == null) {
|
||
return false;
|
||
}
|
||
String lowerSuffix = suffix.toLowerCase();
|
||
return Arrays.asList(
|
||
"doc", "docx", "ppt", "pptx", "pdf", "dwg",
|
||
"dxf", "dwf", "psd", "wps", "xlsx", "xls",
|
||
"rar", "zip"
|
||
).contains(lowerSuffix);
|
||
}
|
||
|
||
/**
|
||
* 校验文件MIME类型是否有效
|
||
* @param contentType 响应头中的Content-Type
|
||
* @param suffix 文件后缀
|
||
* @return 是否有效
|
||
*/
|
||
public static boolean isValidMimeType(String contentType, String suffix) {
|
||
if (contentType == null || suffix == null) {
|
||
return true;
|
||
}
|
||
|
||
// 如果检测到是HTML、文本或JSON格式,则认为是错误响应
|
||
String lowerContentType = contentType.toLowerCase();
|
||
return !lowerContentType.contains("text/html")
|
||
&& !lowerContentType.contains("text/plain")
|
||
&& !lowerContentType.contains("application/json");
|
||
}
|
||
|
||
/**
|
||
* 支持basic 下载方法
|
||
*/
|
||
public static void applyBasicAuthHeaders(HttpHeaders headers, String url) {
|
||
|
||
// 从配置文件读取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);
|
||
}
|
||
}
|
||
}
|