feat(file-upload): 集成新疆油田OSS文件上传功能并优化设备管理
- 集成XjOssUtil实现新疆油田平台OSS文件上传下载功能 - 添加XjOssConfig配置类从Nacos读取OSS相关配置信息 - 实现OAuth2 client_credentials鉴权机制并缓存token到Redis - 支持.tar文件强制使用MyUpload上传,其他文件根据oss_flag配置选择上传方式 - 在WatchDeviceController中禁用移动端解绑手表功能并添加删除设备时的绑定检查 - 添加/sys/upload/down/**和/sys/upload/show/**匿名访问权限支持文件下载预览 - 实现文件下载预览接口支持IOSP平台文件服务并添加安全校验机制 - 在CareUserDetailsParam中添加年份字段默认为当前年份
This commit is contained in:
+3
@@ -1,5 +1,6 @@
|
||||
package com.renkang.anyan.model.dto;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -34,4 +35,6 @@ public class CareUserDetailsParam {
|
||||
|
||||
@Schema(title = "pageType")
|
||||
private String pageType = "plus";
|
||||
|
||||
private int year = DateUtil.thisYear();
|
||||
}
|
||||
|
||||
+7
-1
@@ -207,7 +207,9 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
|
||||
@Operation(summary = "移动端解绑设备(手表)", description = "移动端解绑设备(手表)")
|
||||
@GetMapping("/app/unbundleDevice")
|
||||
public Result unbundleDevice(@RequestParam(name = "watchNo") String watchNo) {
|
||||
return watchDeviceService.unbundleDevice(watchNo);
|
||||
//移动端暂不允许解绑手表
|
||||
return Result.error("请联系健康管理员解绑");
|
||||
// return watchDeviceService.unbundleDevice(watchNo);
|
||||
}
|
||||
|
||||
|
||||
@@ -356,6 +358,10 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
WatchDevice watchDevice = watchDeviceService.getById(id);
|
||||
// 已绑定用户的手表不允许删除
|
||||
if (StringUtils.hasLength(watchDevice.getBindUserId())) {
|
||||
return Result.error("该手表已绑定用户,请先解绑后再删除!");
|
||||
}
|
||||
watchDeviceService.removeById(id);
|
||||
// 删除开关
|
||||
LambdaQueryWrapper<WatchDeviceSwitch> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.apache.http.impl.client.HttpClients;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.ExceptionAssertsUtil;
|
||||
import org.jeecg.common.util.filter.StrAttackFilter;
|
||||
import org.jeecg.util.GlobalManager;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -51,6 +52,8 @@ public class MyUploadUtil {
|
||||
private static String muFileListUploadPath;
|
||||
private static String muDownPath;
|
||||
private static String fileTypeWhiteList;
|
||||
/** 全局配置管理器,用于读取 Redis 中的动态配置 */
|
||||
private static GlobalManager globalManager;
|
||||
// private static final int timeout = 10000;
|
||||
//有些大文件可能需要比较长的时间,这里改成5分钟
|
||||
private static final int socketTimeout = 10*60*1000;
|
||||
@@ -108,6 +111,10 @@ public class MyUploadUtil {
|
||||
MyUploadUtil.fileTypeWhiteList = fileTypeWhiteList;
|
||||
}
|
||||
|
||||
public static void setGlobalManager(GlobalManager globalManager) {
|
||||
MyUploadUtil.globalManager = globalManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
@@ -116,43 +123,52 @@ public class MyUploadUtil {
|
||||
*/
|
||||
|
||||
public static String upload(MultipartFile file, String bizPath) throws Exception {
|
||||
String fileUrl = "";
|
||||
String fileName = file.getOriginalFilename();// 获取文件名
|
||||
checkFileLegitimacy(fileName);
|
||||
//???先屏蔽了
|
||||
// if (StringUtils.hasText(file.getOriginalFilename())) {
|
||||
// String originalFilename = file.getOriginalFilename();
|
||||
// String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
// fileName = fileName + ext;
|
||||
// }
|
||||
bizPath = StrAttackFilter.filter(bizPath);
|
||||
// .tar 文件固定走 MyUpload
|
||||
if (fileName != null && fileName.endsWith(".tar")) {
|
||||
log.info("上传文件为tar文件,使用MyUploadUtil上传");
|
||||
try {
|
||||
return upload(file.getInputStream(), bizPath, fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("upload error {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// 其他文件:oss_flag=1 走 XjOssUtil,否则走 MyUpload
|
||||
String ossFlag = globalManager != null ? globalManager.getConfigValueByKey("oss_flag") : "";
|
||||
if ("1".equals(ossFlag)) {
|
||||
log.info("开启新疆OSS上传");
|
||||
return XjOssUtil.upload(file, bizPath);
|
||||
}
|
||||
log.info("使用MyUploadUtil上传");
|
||||
try {
|
||||
fileUrl = upload(file.getInputStream(), bizPath, fileName);
|
||||
return upload(file.getInputStream(), bizPath, fileName);
|
||||
} catch (Exception e) {
|
||||
fileUrl = null;
|
||||
log.error("upload error", e);
|
||||
log.error("upload error {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件 带缩略图
|
||||
*
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public static String uploadImg(MultipartFile file, String bizPath, Boolean needThumbnailFlag) throws Exception {
|
||||
String fileUrl = "";
|
||||
String fileName = file.getOriginalFilename();// 获取文件名
|
||||
checkFileLegitimacy(fileName);
|
||||
bizPath = StrAttackFilter.filter(bizPath);
|
||||
try {
|
||||
fileUrl = uploadImg(file.getInputStream(), bizPath, fileName, needThumbnailFlag);
|
||||
} catch (Exception e) {
|
||||
log.error("upload error", e);
|
||||
}
|
||||
return fileUrl;
|
||||
}
|
||||
// /**
|
||||
// * 上传文件 带缩略图
|
||||
// *
|
||||
// * @param file
|
||||
// * @return
|
||||
// */
|
||||
// public static String uploadImg(MultipartFile file, String bizPath, Boolean needThumbnailFlag) throws Exception {
|
||||
// String fileUrl = "";
|
||||
// String fileName = file.getOriginalFilename();// 获取文件名
|
||||
// checkFileLegitimacy(fileName);
|
||||
// bizPath = StrAttackFilter.filter(bizPath);
|
||||
// try {
|
||||
// fileUrl = uploadImg(file.getInputStream(), bizPath, fileName, needThumbnailFlag);
|
||||
// } catch (Exception e) {
|
||||
// log.error("upload error", e);
|
||||
// }
|
||||
// return fileUrl;
|
||||
// }
|
||||
|
||||
public static String upload(InputStream stream, String bizPath, String fileName, boolean check) throws Exception {
|
||||
if (check) {
|
||||
@@ -220,145 +236,152 @@ public class MyUploadUtil {
|
||||
* @return
|
||||
*/
|
||||
public static String upload(InputStream stream, String bizPath, String fileName) throws Exception {
|
||||
// 其他文件:oss_flag=1 走 XjOssUtil,否则走 MyUpload
|
||||
String ossFlag = globalManager != null ? globalManager.getConfigValueByKey("oss_flag") : "";
|
||||
if ("1".equals(ossFlag)) {
|
||||
log.info("开启新疆OSS上传");
|
||||
return XjOssUtil.upload(stream, bizPath,fileName);
|
||||
}
|
||||
log.info("使用MyUploadUtil上传");
|
||||
return upload(stream, bizPath, fileName, true);
|
||||
}
|
||||
/**
|
||||
* 上传文件到myupload
|
||||
*
|
||||
* @param stream
|
||||
* @return
|
||||
*/
|
||||
public static String uploadImg(InputStream stream, String bizPath, String fileName, Boolean needThumbnailFlag) throws Exception {
|
||||
//避免重复文件覆盖,这里处理一下
|
||||
if (fileName.indexOf(".") > -1) {
|
||||
//String fileNameLeft = fileName.substring(0, fileName.lastIndexOf("."));
|
||||
String fileNameLeft = UUID.randomUUID().toString();
|
||||
fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
|
||||
} else {
|
||||
fileName += "_" + System.currentTimeMillis();
|
||||
}
|
||||
String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
String path = bizPath + File.separator + nowday;
|
||||
String url = muUrl + "/" + muFileUploadPath;
|
||||
String fileUrl = "";
|
||||
//创建post方法连接实例,在post方法中传入待连接地址
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
CloseableHttpResponse httpResponse = null;
|
||||
// /**
|
||||
// * 上传文件到myupload
|
||||
// *
|
||||
// * @param stream
|
||||
// * @return
|
||||
// */
|
||||
// public static String uploadImg(InputStream stream, String bizPath, String fileName, Boolean needThumbnailFlag) throws Exception {
|
||||
// //避免重复文件覆盖,这里处理一下
|
||||
// if (fileName.indexOf(".") > -1) {
|
||||
// //String fileNameLeft = fileName.substring(0, fileName.lastIndexOf("."));
|
||||
// String fileNameLeft = UUID.randomUUID().toString();
|
||||
// fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
|
||||
// } else {
|
||||
// fileName += "_" + System.currentTimeMillis();
|
||||
// }
|
||||
// String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
// String path = bizPath + File.separator + nowday;
|
||||
// String url = muUrl + "/" + muFileUploadPath;
|
||||
// String fileUrl = "";
|
||||
// //创建post方法连接实例,在post方法中传入待连接地址
|
||||
// CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
// CloseableHttpResponse httpResponse = null;
|
||||
//
|
||||
// byte[] bytes = IoUtil.readBytes(stream);
|
||||
//
|
||||
// try {
|
||||
// URI uri = new URIBuilder(url)
|
||||
// .addParameter("fileName", fileName)
|
||||
// .addParameter("path", path)
|
||||
// .build();
|
||||
// HttpPost httppost = new HttpPost(uri);
|
||||
// RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
|
||||
// httppost.setConfig(requestConfig);
|
||||
// httppost.addHeader("token", muToken);
|
||||
// HttpEntity reqEntity = MultipartEntityBuilder.create()
|
||||
// //设置浏览器兼容模式
|
||||
// .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
// //设置请求的编码格式
|
||||
// .setCharset(StandardCharsets.UTF_8)
|
||||
// .addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
|
||||
// .build();
|
||||
// httppost.setEntity(reqEntity);
|
||||
// httppost.setEntity(reqEntity);
|
||||
//
|
||||
// httpResponse = httpClient.execute(httppost);
|
||||
//
|
||||
//
|
||||
// int backCode = httpResponse.getStatusLine().getStatusCode();
|
||||
// if (backCode != HttpStatus.SC_OK) {
|
||||
// log.error("上传文件失败" + backCode);
|
||||
// } else {
|
||||
// fileUrl = path + File.separator + fileName;
|
||||
// fileUrl = relaceSlash(fileUrl);
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
// log.error("文件服务器连接失败", e);
|
||||
// } finally {
|
||||
// //释放资源
|
||||
// try {
|
||||
// httpClient.close();
|
||||
// if (httpResponse != null) {
|
||||
// httpResponse.close();
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
//// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (needThumbnailFlag) {
|
||||
// uploadThumbnail(url, bytes, fileName, path);
|
||||
// }
|
||||
//
|
||||
// return fileUrl;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 上传缩略图到myupload
|
||||
// *
|
||||
// * @return
|
||||
// */
|
||||
// private static void uploadThumbnail(String url, byte[] bytes, String fileName, String path) {
|
||||
// //创建post方法连接实例,在post方法中传入待连接地址
|
||||
// CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
// CloseableHttpResponse httpResponse = null;
|
||||
// try {
|
||||
// String thumbnailName = CommonConstant.THUMBNAIL_PREFIX + fileName;
|
||||
//
|
||||
// BufferedImage bufferedImage = Thumbnails.of(new ByteArrayInputStream(bytes))
|
||||
// .scale(CommonConstant.THUMBNAIL_SCALE)
|
||||
// .asBufferedImage();
|
||||
//
|
||||
// ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
// ImageIO.write(bufferedImage, "png", os);
|
||||
// InputStream is = new ByteArrayInputStream(os.toByteArray());
|
||||
// URI uri = new URIBuilder(url)
|
||||
// .addParameter("fileName", thumbnailName)
|
||||
// .addParameter("path", path)
|
||||
// .build();
|
||||
// HttpPost httppost = new HttpPost(uri);
|
||||
// RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
|
||||
// httppost.setConfig(requestConfig);
|
||||
// httppost.addHeader("token", muToken);
|
||||
// HttpEntity reqEntity = MultipartEntityBuilder.create()
|
||||
// //设置浏览器兼容模式
|
||||
// .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
// //设置请求的编码格式
|
||||
// .setCharset(Consts.UTF_8)
|
||||
//
|
||||
// .addBinaryBody("file", is, ContentType.APPLICATION_OCTET_STREAM, thumbnailName)
|
||||
// .build();
|
||||
// httppost.setEntity(reqEntity);
|
||||
//
|
||||
// httpResponse = httpClient.execute(httppost);
|
||||
//
|
||||
// int backCode = httpResponse.getStatusLine().getStatusCode();
|
||||
// if (backCode != HttpStatus.SC_OK) {
|
||||
// log.error("上传缩略图失败" + backCode);
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
// log.error("文件服务器连接失败", e);
|
||||
// } catch (URISyntaxException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// } finally {
|
||||
// //释放资源
|
||||
// try {
|
||||
// httpClient.close();
|
||||
// if (httpResponse != null) {
|
||||
// httpResponse.close();
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
byte[] bytes = IoUtil.readBytes(stream);
|
||||
|
||||
try {
|
||||
URI uri = new URIBuilder(url)
|
||||
.addParameter("fileName", fileName)
|
||||
.addParameter("path", path)
|
||||
.build();
|
||||
HttpPost httppost = new HttpPost(uri);
|
||||
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
|
||||
httppost.setConfig(requestConfig);
|
||||
httppost.addHeader("token", muToken);
|
||||
HttpEntity reqEntity = MultipartEntityBuilder.create()
|
||||
//设置浏览器兼容模式
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
//设置请求的编码格式
|
||||
.setCharset(StandardCharsets.UTF_8)
|
||||
.addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
|
||||
.build();
|
||||
httppost.setEntity(reqEntity);
|
||||
httppost.setEntity(reqEntity);
|
||||
|
||||
httpResponse = httpClient.execute(httppost);
|
||||
|
||||
|
||||
int backCode = httpResponse.getStatusLine().getStatusCode();
|
||||
if (backCode != HttpStatus.SC_OK) {
|
||||
log.error("上传文件失败" + backCode);
|
||||
} else {
|
||||
fileUrl = path + File.separator + fileName;
|
||||
fileUrl = relaceSlash(fileUrl);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("文件服务器连接失败", e);
|
||||
} finally {
|
||||
//释放资源
|
||||
try {
|
||||
httpClient.close();
|
||||
if (httpResponse != null) {
|
||||
httpResponse.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (needThumbnailFlag) {
|
||||
uploadThumbnail(url, bytes, fileName, path);
|
||||
}
|
||||
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传缩略图到myupload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static void uploadThumbnail(String url, byte[] bytes, String fileName, String path) {
|
||||
//创建post方法连接实例,在post方法中传入待连接地址
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
CloseableHttpResponse httpResponse = null;
|
||||
try {
|
||||
String thumbnailName = CommonConstant.THUMBNAIL_PREFIX + fileName;
|
||||
|
||||
BufferedImage bufferedImage = Thumbnails.of(new ByteArrayInputStream(bytes))
|
||||
.scale(CommonConstant.THUMBNAIL_SCALE)
|
||||
.asBufferedImage();
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
ImageIO.write(bufferedImage, "png", os);
|
||||
InputStream is = new ByteArrayInputStream(os.toByteArray());
|
||||
URI uri = new URIBuilder(url)
|
||||
.addParameter("fileName", thumbnailName)
|
||||
.addParameter("path", path)
|
||||
.build();
|
||||
HttpPost httppost = new HttpPost(uri);
|
||||
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
|
||||
httppost.setConfig(requestConfig);
|
||||
httppost.addHeader("token", muToken);
|
||||
HttpEntity reqEntity = MultipartEntityBuilder.create()
|
||||
//设置浏览器兼容模式
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
//设置请求的编码格式
|
||||
.setCharset(Consts.UTF_8)
|
||||
|
||||
.addBinaryBody("file", is, ContentType.APPLICATION_OCTET_STREAM, thumbnailName)
|
||||
.build();
|
||||
httppost.setEntity(reqEntity);
|
||||
|
||||
httpResponse = httpClient.execute(httppost);
|
||||
|
||||
int backCode = httpResponse.getStatusLine().getStatusCode();
|
||||
if (backCode != HttpStatus.SC_OK) {
|
||||
log.error("上传缩略图失败" + backCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("文件服务器连接失败", e);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
//释放资源
|
||||
try {
|
||||
httpClient.close();
|
||||
if (httpResponse != null) {
|
||||
httpResponse.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMuFileExistPath() {
|
||||
return muFileExistPath;
|
||||
}
|
||||
// public String getMuFileExistPath() {
|
||||
// return muFileExistPath;
|
||||
// }
|
||||
|
||||
public static void setMuFileExistPath(String muFileExistPath) {
|
||||
MyUploadUtil.muFileExistPath = muFileExistPath;
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.http.entity.mime.HttpMultipartMode;
|
||||
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jeecg.util.RedisClientUtil;
|
||||
import org.jeecg.util.RedisKeyPrefix;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.jeecg.common.exception.ExceptionAssertsUtil;
|
||||
import org.jeecg.common.util.filter.StrAttackFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 新疆油田平台(IOSP)OSS 文件工具类
|
||||
* <p>
|
||||
* 功能:
|
||||
* 1. OAuth2 client_credentials 鉴权,token 缓存到 Redis,401 时自动重新鉴权
|
||||
* 2. 文件上传(支持 InputStream 和 MultipartFile)
|
||||
* 3. 文件下载(通过 fileKey 获取文件流,处理 302 重定向)
|
||||
* </p>
|
||||
* <p>
|
||||
* 配置项(Nacos 中配置,前缀 xj.oss):
|
||||
* - gateway-url 网关地址,如 http://api.iosp.ydpt.tech
|
||||
* - client-id OAuth2 客户端ID
|
||||
* - client-secret OAuth2 客户端密钥
|
||||
* - organization-id 组织ID(上传/下载路径参数)
|
||||
* - config-code 配置Code(上传/下载路径参数)
|
||||
* - base-url-prefix 文件基础路径前缀(可选,用于截取 Location 中的相对路径)
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class XjOssUtil {
|
||||
|
||||
// ===================== 静态配置字段(由 XjOssConfig 注入) =====================
|
||||
|
||||
/** 网关地址,如 http://api.iosp.ydpt.tech */
|
||||
private static String gatewayUrl;
|
||||
/** OAuth2 客户端ID */
|
||||
private static String clientId;
|
||||
/** OAuth2 客户端密钥 */
|
||||
private static String clientSecret;
|
||||
/** 组织ID */
|
||||
private static String organizationId;
|
||||
/** 配置Code */
|
||||
private static String configCode;
|
||||
/** 文件基础路径前缀,如 http://11.71.10.44:8060/wbwj/scyx/scfz/aygc/jkgl/0 */
|
||||
private static String baseUrlPrefix;
|
||||
/** 允许上传的文件类型白名单,逗号分隔,如 jpg,png,pdf */
|
||||
private static String fileTypeWhiteList;
|
||||
/** 授权上传目录,如 scyx/scfz/aygc/jkgl/ */
|
||||
private static String directory;
|
||||
|
||||
// ===================== Redis 工具(由 XjOssConfig 注入) =====================
|
||||
|
||||
private static RedisClientUtil redisClientUtil;
|
||||
|
||||
// ===================== 常量 =====================
|
||||
|
||||
/** Redis key 前缀(不含过期时间,仅用于 delete/get 操作) */
|
||||
private static final RedisKeyPrefix REDIS_PREFIX_NO_TTL = new RedisKeyPrefix("xj:oss");
|
||||
/** Redis 中存储 token 的 key */
|
||||
private static final String TOKEN_KEY = "access_token";
|
||||
/** token 过期时间预留缓冲(秒),提前刷新避免临界过期 */
|
||||
private static final int TOKEN_EXPIRE_BUFFER = 60;
|
||||
/** HTTP 连接超时(毫秒) */
|
||||
private static final int CONNECT_TIMEOUT = 30_000;
|
||||
/** HTTP Socket 超时(毫秒),大文件上传需要较长时间 */
|
||||
private static final int SOCKET_TIMEOUT = 10 * 60_000;
|
||||
private static final String DIR = "/jkgl/";
|
||||
|
||||
// ===================== Setter(供 XjOssConfig 注入) =====================
|
||||
|
||||
public static void setGatewayUrl(String gatewayUrl) {
|
||||
XjOssUtil.gatewayUrl = gatewayUrl;
|
||||
}
|
||||
|
||||
public static void setClientId(String clientId) {
|
||||
XjOssUtil.clientId = clientId;
|
||||
}
|
||||
|
||||
public static void setClientSecret(String clientSecret) {
|
||||
XjOssUtil.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public static void setOrganizationId(String organizationId) {
|
||||
XjOssUtil.organizationId = organizationId;
|
||||
}
|
||||
|
||||
public static void setConfigCode(String configCode) {
|
||||
XjOssUtil.configCode = configCode;
|
||||
}
|
||||
|
||||
public static void setBaseUrlPrefix(String baseUrlPrefix) {
|
||||
XjOssUtil.baseUrlPrefix = baseUrlPrefix;
|
||||
}
|
||||
|
||||
public static void setFileTypeWhiteList(String fileTypeWhiteList) {
|
||||
XjOssUtil.fileTypeWhiteList = fileTypeWhiteList;
|
||||
}
|
||||
|
||||
public static void setDirectory(String directory) {
|
||||
XjOssUtil.directory = directory;
|
||||
}
|
||||
|
||||
public static void setRedisClientUtil(RedisClientUtil redisClientUtil) {
|
||||
XjOssUtil.redisClientUtil = redisClientUtil;
|
||||
}
|
||||
|
||||
// ===================== 鉴权 =====================
|
||||
|
||||
/**
|
||||
* 向 IOSP 网关发起 OAuth2 鉴权,获取 access_token 并存入 Redis
|
||||
* <p>
|
||||
* 接口:POST {gatewayUrl}/oauth/oauth/token
|
||||
* 参数:grant_type=client_credentials, client_id, client_secret
|
||||
* </p>
|
||||
*
|
||||
* @return 获取到的 access_token,失败返回 null
|
||||
*/
|
||||
public static String authenticate() {
|
||||
String url = gatewayUrl + "/oauth/oauth/token";
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
HttpPost post = new HttpPost(url);
|
||||
post.setConfig(buildRequestConfig());
|
||||
// 使用 multipart/form-data 提交鉴权参数
|
||||
post.setEntity(MultipartEntityBuilder.create()
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
.setCharset(StandardCharsets.UTF_8)
|
||||
.addTextBody("grant_type", "client_credentials")
|
||||
.addTextBody("client_id", clientId)
|
||||
.addTextBody("client_secret", clientSecret)
|
||||
.build());
|
||||
|
||||
response = httpClient.execute(post);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != HttpStatus.SC_OK) {
|
||||
log.error("[XjOssUtil] 鉴权失败,HTTP状态码: {}", statusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
JSONObject json = JSON.parseObject(body);
|
||||
String token = json.getString("access_token");
|
||||
// expires_in 单位为秒,减去缓冲时间后存入 Redis
|
||||
int expiresIn = json.getIntValue("expires_in");
|
||||
int ttl = Math.max(expiresIn - TOKEN_EXPIRE_BUFFER, 30);
|
||||
|
||||
// 存入 Redis,TTL = expires_in - 缓冲;动态创建带 TTL 的 RedisKeyPrefix
|
||||
RedisKeyPrefix prefixWithTtl = new RedisKeyPrefix(ttl, "xj:oss");
|
||||
redisClientUtil.set(prefixWithTtl, TOKEN_KEY, token);
|
||||
log.info("[XjOssUtil] 鉴权成功,token 有效期 {}s(含缓冲)", ttl);
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
log.error("[XjOssUtil] 鉴权异常", e);
|
||||
return null;
|
||||
} finally {
|
||||
closeQuietly(httpClient, response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的 access_token
|
||||
* <p>
|
||||
* 优先从 Redis 读取缓存,不存在时自动重新鉴权
|
||||
* </p>
|
||||
*
|
||||
* @return 有效的 access_token,失败返回 null
|
||||
*/
|
||||
public static String getToken() {
|
||||
String token = redisClientUtil.get(REDIS_PREFIX_NO_TTL, TOKEN_KEY, String.class);
|
||||
if (token == null) {
|
||||
log.info("[XjOssUtil] Redis 中 token 不存在或已过期,重新鉴权");
|
||||
token = authenticate();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// ===================== 文件上传 =====================
|
||||
|
||||
/**
|
||||
* 上传文件(MultipartFile 方式)
|
||||
*
|
||||
* @param file 前端上传的文件
|
||||
* @param bizPath 业务子路径,如 import、temp,拼接在授权目录之后
|
||||
* @return 上传成功后的文件 URL,失败返回 null
|
||||
*/
|
||||
public static String upload(MultipartFile file, String bizPath) {
|
||||
try {
|
||||
return upload(file.getInputStream(), bizPath, file.getOriginalFilename());
|
||||
} catch (Exception e) {
|
||||
log.error("[XjOssUtil] MultipartFile 上传失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件(InputStream 方式)
|
||||
*
|
||||
* @param stream 文件输入流
|
||||
* @param bizPath 业务子路径,如 import、temp,拼接在授权目录之后
|
||||
* @param fileName 文件名,如 test.png
|
||||
* @return 上传成功后的文件 URL,失败返回 null
|
||||
*/
|
||||
public static String upload(InputStream stream, String bizPath, String fileName) {
|
||||
checkFileLegitimacy(fileName);
|
||||
// 过滤上传文件夹名特殊字符,防止路径攻击
|
||||
bizPath = StrAttackFilter.filter(bizPath);
|
||||
// 构造上传 URL:/hfle/v2/{organizationId}/files/{configCode}/multipart
|
||||
String url = gatewayUrl + "/hfle/v2/" + organizationId + "/files/" + configCode + "/multipart";
|
||||
|
||||
// 避免重复文件覆盖,追加时间戳
|
||||
if (fileName.contains(".")) {
|
||||
String fileNameLeft = fileName.substring(0, fileName.lastIndexOf("."));
|
||||
fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
|
||||
} else {
|
||||
fileName += "_" + System.currentTimeMillis();
|
||||
}
|
||||
String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
// 最终 fileName = bizPath/yyyyMMdd/originalName_timestamp.ext
|
||||
final String finalFileName = DIR + bizPath + "/" + nowday + "/" + fileName;
|
||||
|
||||
return doUpload(stream, url, finalFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实际上传请求,401 时刷新 token 重试一次
|
||||
* <p>fileName 为已处理好的完整路径,不再做二次处理</p>
|
||||
*/
|
||||
private static String doUpload(InputStream stream, String url, String fileName) {
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
URI uri = new URIBuilder(url)
|
||||
.addParameter("directory", directory)
|
||||
.addParameter("fileName", fileName)
|
||||
.addParameter("docType", "0")
|
||||
.build();
|
||||
|
||||
HttpPost post = new HttpPost(uri);
|
||||
post.setConfig(buildRequestConfig());
|
||||
post.addHeader("Authorization", "Bearer " + getToken());
|
||||
post.addHeader("accept", "application/json;charset=utf-8");
|
||||
post.setEntity(MultipartEntityBuilder.create()
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
.setCharset(StandardCharsets.UTF_8)
|
||||
.addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
|
||||
.build());
|
||||
|
||||
response = httpClient.execute(post);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
|
||||
// 401 表示 token 过期,清除缓存后重试一次(fileName 已处理,不会重复拼接)
|
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
|
||||
log.warn("[XjOssUtil] 上传时 token 过期,重新鉴权后重试");
|
||||
closeQuietly(httpClient, response);
|
||||
redisClientUtil.delete(REDIS_PREFIX_NO_TTL, TOKEN_KEY);
|
||||
return doUpload(stream, url, fileName);
|
||||
}
|
||||
|
||||
if (statusCode != HttpStatus.SC_OK) {
|
||||
log.error("[XjOssUtil] 文件上传失败,HTTP状态码: {}", statusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
String fileUrl = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
// 接口可能返回 HTTP 200 但 body 为业务错误 JSON,需单独判断
|
||||
JSONObject resultJson = null;
|
||||
try {
|
||||
resultJson = JSON.parseObject(fileUrl);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (resultJson != null && Boolean.TRUE.equals(resultJson.getBoolean("failed"))) {
|
||||
log.error("[XjOssUtil] 文件上传业务失败: {}", fileUrl);
|
||||
return null;
|
||||
}
|
||||
String fileKey = extractFileKey(fileUrl);
|
||||
log.info("[XjOssUtil] 文件上传成功,fileKey: {}", fileKey);
|
||||
return fileKey;
|
||||
} catch (Exception e) {
|
||||
log.error("[XjOssUtil] 文件上传异常", e);
|
||||
return null;
|
||||
} finally {
|
||||
closeQuietly(httpClient, response);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 文件下载 =====================
|
||||
|
||||
/**
|
||||
* 下载文件,返回文件输入流
|
||||
* <p>
|
||||
* 入参为短 fileKey,如 808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* 内部自动拼接完整路径:{directory}0/{fileKey}
|
||||
* </p>
|
||||
*
|
||||
* @param fileKey 短文件 key,如 808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* @return 文件输入流,调用方负责关闭;失败返回 null
|
||||
*/
|
||||
public static InputStream down(String fileKey) {
|
||||
String locationUrl = getLocationUrl(buildFullFileKey(fileKey));
|
||||
if (locationUrl == null) {
|
||||
return null;
|
||||
}
|
||||
CloseableHttpClient downloadClient = HttpClients.createDefault();
|
||||
try {
|
||||
HttpGet downloadGet = new HttpGet(locationUrl);
|
||||
downloadGet.setConfig(buildRequestConfig());
|
||||
CloseableHttpResponse downloadResponse = downloadClient.execute(downloadGet);
|
||||
int downloadStatus = downloadResponse.getStatusLine().getStatusCode();
|
||||
if (downloadStatus != HttpStatus.SC_OK) {
|
||||
log.error("[XjOssUtil] 获取文件流失败,HTTP状态码: {}", downloadStatus);
|
||||
closeQuietly(downloadClient, downloadResponse);
|
||||
return null;
|
||||
}
|
||||
// 返回包装流,关闭时自动释放 HTTP 资源
|
||||
return new HttpEntityInputStream(downloadResponse.getEntity().getContent(), downloadClient, downloadResponse);
|
||||
} catch (Exception e) {
|
||||
log.error("[XjOssUtil] 文件下载异常", e);
|
||||
closeQuietly(downloadClient, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件预览地址(Location 重定向 URL)
|
||||
* <p>
|
||||
* 入参为短 fileKey,如 808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* 内部自动拼接完整路径:{directory}0/{fileKey}
|
||||
* 返回的 URL 可直接用于浏览器预览或前端展示
|
||||
* </p>
|
||||
*
|
||||
* @param fileKey 短文件 key,如 808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* @return 预览 URL,失败返回 null
|
||||
*/
|
||||
public static String show(String fileKey) {
|
||||
return getLocationUrl(buildFullFileKey(fileKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将短 fileKey 拼接为下载接口所需的完整路径
|
||||
* <p>规则:{directory}0/{fileKey},示例:scyx/scfz/aygc/jkgl/0/808fec...@aa.png</p>
|
||||
*/
|
||||
private static String buildFullFileKey(String fileKey) {
|
||||
String dir = directory.endsWith("/") ? directory : directory + "/";
|
||||
return dir + "0/" + fileKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 download-by-key 接口,获取 302 重定向的 Location URL
|
||||
* <p>401 时自动刷新 token 重试一次</p>
|
||||
*
|
||||
* @param fullFileKey 完整 fileKey,如 scyx/scfz/aygc/jkgl/0/808fec...@aa.png
|
||||
* @return Location URL,失败返回 null
|
||||
*/
|
||||
private static String getLocationUrl(String fullFileKey) {
|
||||
String url = gatewayUrl + "/hfle/v2/" + organizationId + "/files/" + configCode + "/download-by-key";
|
||||
CloseableHttpClient noRedirectClient = org.apache.http.impl.client.HttpClientBuilder.create()
|
||||
.disableRedirectHandling()
|
||||
.build();
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
URI uri = new URIBuilder(url)
|
||||
.addParameter("fileKey", fullFileKey)
|
||||
.build();
|
||||
HttpGet get = new HttpGet(uri);
|
||||
get.setConfig(buildRequestConfig());
|
||||
String token = getToken();
|
||||
get.addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
// 打印等效 curl 命令,便于调试
|
||||
log.info("[XjOssUtil] curl --request GET \\\n --url '{}' \\\n --header 'Authorization: Bearer {}'",
|
||||
uri.toString(), token);
|
||||
|
||||
response = noRedirectClient.execute(get);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
|
||||
// 401 表示 token 过期,重新鉴权后重试一次
|
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
|
||||
log.warn("[XjOssUtil] 获取 Location 时 token 过期,重新鉴权后重试");
|
||||
closeQuietly(noRedirectClient, response);
|
||||
redisClientUtil.delete(REDIS_PREFIX_NO_TTL, TOKEN_KEY);
|
||||
return getLocationUrl(fullFileKey);
|
||||
}
|
||||
|
||||
if (statusCode != HttpStatus.SC_MOVED_TEMPORARILY && statusCode != HttpStatus.SC_SEE_OTHER) {
|
||||
log.error("[XjOssUtil] download-by-key 返回非预期状态码: {}", statusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
Header locationHeader = response.getFirstHeader("Location");
|
||||
if (locationHeader == null) {
|
||||
log.error("[XjOssUtil] download-by-key 未返回 Location 响应头");
|
||||
return null;
|
||||
}
|
||||
String locationUrl = locationHeader.getValue();
|
||||
log.info("[XjOssUtil] 获取到 Location: {}", locationUrl);
|
||||
return locationUrl;
|
||||
} catch (Exception e) {
|
||||
log.error("[XjOssUtil] 获取 Location 异常", e);
|
||||
return null;
|
||||
} finally {
|
||||
closeQuietly(noRedirectClient, response);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 私有工具方法 =====================
|
||||
|
||||
/**
|
||||
* 文件合法性校验(文件类型白名单)
|
||||
* <p>
|
||||
* 白名单为空时跳过校验;文件名无扩展名或扩展名不在白名单中时抛出异常。
|
||||
* </p>
|
||||
*
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
private static void checkFileLegitimacy(String fileName) {
|
||||
if (StrUtil.isNotEmpty(fileTypeWhiteList)) {
|
||||
String extName = FileNameUtil.extName(fileName);
|
||||
if (StrUtil.isBlank(extName)) {
|
||||
ExceptionAssertsUtil.fail("文件上传失败:不能识别的文件类型!");
|
||||
}
|
||||
List<String> fileTypeList = Arrays.asList(fileTypeWhiteList.split(","));
|
||||
// 统一转小写比对,忽略大小写差异
|
||||
if (!fileTypeList.contains(extName.toLowerCase())) {
|
||||
ExceptionAssertsUtil.fail("文件上传失败:不支持的文件类型!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上传接口返回的完整 URL 中截取 fileKey
|
||||
* <p>
|
||||
* 返回示例:https://wbwj.http://11.71.10.44:8060/scyx/.../0/808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* 截取结果:808fec572eda4428a84c8b4c4f07f511@aa.png
|
||||
* 规则:取 @ 符号前最后一个 / 之后的所有内容
|
||||
* </p>
|
||||
*
|
||||
* @param fileUrl 接口返回的完整 URL
|
||||
* @return fileKey,解析失败时返回原始 URL
|
||||
*/
|
||||
private static String extractFileKey(String fileUrl) {
|
||||
int atIndex = fileUrl.indexOf("@");
|
||||
if (atIndex < 0) {
|
||||
return fileUrl;
|
||||
}
|
||||
int slashIndex = fileUrl.lastIndexOf("/", atIndex);
|
||||
if (slashIndex < 0) {
|
||||
return fileUrl;
|
||||
}
|
||||
return fileUrl.substring(slashIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建统一的 HTTP 请求配置
|
||||
*/
|
||||
private static RequestConfig buildRequestConfig() {
|
||||
return RequestConfig.custom()
|
||||
.setConnectTimeout(CONNECT_TIMEOUT)
|
||||
.setSocketTimeout(SOCKET_TIMEOUT)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 静默关闭 HTTP 资源,忽略异常
|
||||
*/
|
||||
private static void closeQuietly(CloseableHttpClient client, CloseableHttpResponse response) {
|
||||
try {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
try {
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 内部类:包装 InputStream,关闭时释放 HTTP 资源 =====================
|
||||
|
||||
/**
|
||||
* 包装 HTTP 响应的 InputStream,关闭时自动释放 HTTP 连接资源
|
||||
*/
|
||||
private static class HttpEntityInputStream extends InputStream {
|
||||
|
||||
private final InputStream delegate;
|
||||
private final CloseableHttpClient httpClient;
|
||||
private final CloseableHttpResponse httpResponse;
|
||||
|
||||
HttpEntityInputStream(InputStream delegate, CloseableHttpClient httpClient, CloseableHttpResponse httpResponse) {
|
||||
this.delegate = delegate;
|
||||
this.httpClient = httpClient;
|
||||
this.httpResponse = httpResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return delegate.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return delegate.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
delegate.close();
|
||||
} finally {
|
||||
closeQuietly(httpClient, httpResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package org.jeecg.config.oss;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.MyUploadUtil;
|
||||
import org.jeecg.util.GlobalManager;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -29,6 +31,9 @@ public class MyUploadConfig {
|
||||
@Value(value = "${jeecg.myupload.mu_file_type_white_list}")
|
||||
private String fileTypeWhiteList;
|
||||
|
||||
@Autowired
|
||||
private GlobalManager globalManager;
|
||||
|
||||
@Bean
|
||||
public void initMu() {
|
||||
MyUploadUtil.setMuUrl(muUrl);
|
||||
@@ -39,6 +44,7 @@ public class MyUploadConfig {
|
||||
MyUploadUtil.setMuFileListUploadPath(muFileListUploadPath);
|
||||
MyUploadUtil.setMuDownPath(muDownPath);
|
||||
MyUploadUtil.setFileTypeWhiteList(fileTypeWhiteList);
|
||||
MyUploadUtil.setGlobalManager(globalManager);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.jeecg.config.oss;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.XjOssUtil;
|
||||
import org.jeecg.util.RedisClientUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 新疆油田平台(IOSP)OSS 配置类
|
||||
* <p>
|
||||
* 从 Nacos 读取配置并注入到 XjOssUtil 静态字段中。
|
||||
* 对应 Nacos 配置前缀:xj.oss
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class XjOssConfig {
|
||||
|
||||
/** 网关地址,如 http://api.iosp.ydpt.tech */
|
||||
@Value(value = "${variable.anyan.host}")
|
||||
private String gatewayUrl;
|
||||
|
||||
/** OAuth2 客户端ID */
|
||||
@Value(value = "${variable.anyan.client-id}")
|
||||
private String clientId;
|
||||
|
||||
/** OAuth2 客户端密钥 */
|
||||
@Value(value = "${variable.anyan.client-secret}")
|
||||
private String clientSecret;
|
||||
|
||||
/** 组织ID(上传/下载路径参数) */
|
||||
@Value(value = "${variable.anyan.organization-id}")
|
||||
private String organizationId;
|
||||
|
||||
/** 配置Code(上传/下载路径参数) */
|
||||
@Value(value = "${variable.anyan.config-code}")
|
||||
private String configCode;
|
||||
|
||||
/**
|
||||
* 文件基础路径前缀(可选)
|
||||
* 示例:http://11.71.10.44:8060/wbwj/scyx/scfz/aygc/jkgl/0
|
||||
* 用于从完整 URL 中截取相对 fileKey
|
||||
*/
|
||||
@Value(value = "${variable.anyan.base-url-prefix:}")
|
||||
private String baseUrlPrefix;
|
||||
|
||||
/**
|
||||
* 允许上传的文件类型白名单,逗号分隔(可选)
|
||||
* 示例:jpg,jpeg,png,pdf,doc,docx
|
||||
* 为空时不做类型限制
|
||||
*/
|
||||
@Value(value = "${variable.anyan.file-type-white-list:}")
|
||||
private String fileTypeWhiteList;
|
||||
|
||||
/**
|
||||
* 授权上传目录,需与 IOSP 平台授权的目录一致
|
||||
* 示例:scyx/scfz/aygc/jkgl/
|
||||
*/
|
||||
@Value(value = "${variable.anyan.directory}")
|
||||
private String directory;
|
||||
|
||||
@Autowired
|
||||
private RedisClientUtil redisClientUtil;
|
||||
|
||||
/**
|
||||
* 初始化 XjOssUtil 静态配置
|
||||
*/
|
||||
@Bean
|
||||
public void initXjOss() {
|
||||
XjOssUtil.setGatewayUrl(gatewayUrl);
|
||||
XjOssUtil.setClientId(clientId);
|
||||
XjOssUtil.setClientSecret(clientSecret);
|
||||
XjOssUtil.setOrganizationId(organizationId);
|
||||
XjOssUtil.setConfigCode(configCode);
|
||||
XjOssUtil.setBaseUrlPrefix(baseUrlPrefix);
|
||||
XjOssUtil.setFileTypeWhiteList(fileTypeWhiteList);
|
||||
XjOssUtil.setDirectory(directory);
|
||||
XjOssUtil.setRedisClientUtil(redisClientUtil);
|
||||
log.info("[XjOssConfig] IOSP OSS 配置初始化完成,网关地址: {}", gatewayUrl);
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,8 @@ public class ShiroConfig {
|
||||
|
||||
filterChainDefinitionMap.put("/version/getSysVersionDetailByPackageName", "anon");
|
||||
filterChainDefinitionMap.put("/sys/api/notice/selectIosUpdate", "anon");
|
||||
filterChainDefinitionMap.put("/sys/upload/down/**", "anon");
|
||||
filterChainDefinitionMap.put("/sys/upload/show/**", "anon");
|
||||
|
||||
// 添加自己的过滤器并且取名为jwt
|
||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||
|
||||
+100
@@ -1,10 +1,12 @@
|
||||
package org.jeecg.modules.system.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.MyUploadUtil;
|
||||
import org.jeecg.common.util.XjOssUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.oss.service.IOssFileService;
|
||||
import org.jeecg.modules.system.bean.request.UploadFilsDTO;
|
||||
@@ -15,6 +17,11 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -89,6 +96,9 @@ public class SysUploadController {
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
if (StrUtil.isBlank(fileUrl)){
|
||||
return Result.error("上传失败,请检查配置信息是否正确!");
|
||||
}
|
||||
Map map = new HashMap();
|
||||
map.put("url", fileUrl);
|
||||
return Result.OK(map);
|
||||
@@ -115,4 +125,94 @@ public class SysUploadController {
|
||||
return Result.OK(urlList);
|
||||
}
|
||||
|
||||
@Operation(summary = "IOSP文件下载", description = "根据 fileKey 下载文件,如 sys/upload/down/808fec...@aa.png")
|
||||
@RequestMapping(value = "/down/**", method = RequestMethod.GET)
|
||||
public void fileDown(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String fileKey = extractPathParam(request, "/down/");
|
||||
if (isIllegalFileKey(fileKey)) {
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "fileKey 格式非法");
|
||||
return;
|
||||
}
|
||||
try (InputStream in = XjOssUtil.down(fileKey)) {
|
||||
if (in == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在或下载失败");
|
||||
return;
|
||||
}
|
||||
// 从 fileKey 中取文件名(@ 后面的部分)
|
||||
String fileName = fileKey.contains("@") ? fileKey.substring(fileKey.indexOf("@") + 1) : fileKey;
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
|
||||
try (OutputStream out = response.getOutputStream()) {
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[SysUploadController] IOSP文件下载失败, fileKey={}", fileKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "IOSP文件预览", description = "根据 fileKey 获取预览地址,如 sys/upload/show/808fec...@aa.png")
|
||||
@RequestMapping(value = "/show/**", method = RequestMethod.GET)
|
||||
public void fileShow(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String fileKey = extractPathParam(request, "/show/");
|
||||
if (isIllegalFileKey(fileKey)) {
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "fileKey 格式非法");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String url = XjOssUtil.show(fileKey);
|
||||
if (url == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在或获取预览地址失败");
|
||||
return;
|
||||
}
|
||||
// 重定向到真实预览地址
|
||||
response.sendRedirect(url);
|
||||
} catch (Exception e) {
|
||||
log.error("[SysUploadController] IOSP文件预览失败, fileKey={}", fileKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求路径中提取 fileKey
|
||||
* <p>截取 marker 之后的路径部分,并做 URL 解码</p>
|
||||
*/
|
||||
private String extractPathParam(HttpServletRequest request, String marker) {
|
||||
String uri = request.getRequestURI();
|
||||
int idx = uri.indexOf(marker);
|
||||
if (idx < 0) {
|
||||
return "";
|
||||
}
|
||||
String raw = uri.substring(idx + marker.length());
|
||||
try {
|
||||
return java.net.URLDecoder.decode(raw, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 fileKey 是否存在路径穿透风险
|
||||
* <p>
|
||||
* 拦截 ../、..\ 及其 URL 编码变体(%2e%2e%2f 等),防止跳出授权目录
|
||||
* </p>
|
||||
*/
|
||||
private boolean isIllegalFileKey(String fileKey) {
|
||||
if (oConvertUtils.isEmpty(fileKey)) {
|
||||
return true;
|
||||
}
|
||||
// 先做 URL 解码,防止编码绕过
|
||||
String decoded;
|
||||
try {
|
||||
decoded = java.net.URLDecoder.decode(fileKey, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
return decoded.contains("../") || decoded.contains("..\\")
|
||||
|| decoded.startsWith("..") || decoded.contains("/..");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user