yxk-20260709
文件在线预览功能
This commit is contained in:
@@ -256,6 +256,7 @@ public class ShiroConfig {
|
|||||||
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
||||||
shiroFilterFactoryBean.setFilters(filterMap);
|
shiroFilterFactoryBean.setFilters(filterMap);
|
||||||
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
||||||
|
filterChainDefinitionMap.put("/sys/file/previewDownload", "anon");//kkFileView short-lived preview stream
|
||||||
filterChainDefinitionMap.put("/**", "jwt");
|
filterChainDefinitionMap.put("/**", "jwt");
|
||||||
|
|
||||||
// 未授权界面返回JSON
|
// 未授权界面返回JSON
|
||||||
|
|||||||
+151
@@ -59,6 +59,9 @@ public class FileController {
|
|||||||
@Value(value = "${jeecg.appName}")
|
@Value(value = "${jeecg.appName}")
|
||||||
private String applicationName;
|
private String applicationName;
|
||||||
|
|
||||||
|
private static final String FILE_PREVIEW_REDIS_PREFIX = "sys:file:preview:";
|
||||||
|
private static final long FILE_PREVIEW_TOKEN_EXPIRE_SECONDS = 600L;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -451,6 +454,92 @@ public class FileController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Operation(summary = "生成附件预览短时地址")
|
||||||
|
@GetMapping(value = "/previewUrl")
|
||||||
|
public Result<String> previewUrl(@RequestParam String id, HttpServletRequest request) {
|
||||||
|
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
|
if (sysUser == null) {
|
||||||
|
return Result.error("当前用户失效,无法预览文件,请重新登录");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(id)) {
|
||||||
|
return Result.error("文件ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> fileInfo = this.sysBaseAPI.queryFileMapById(id);
|
||||||
|
if (fileInfo == null || fileInfo.isEmpty()) {
|
||||||
|
return Result.error("文件不存在或已被删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer userSecretLevel = sysUser.getUserSecurityLevel();
|
||||||
|
int fileSecretLevel = parseSecretLevel(fileInfo.get("secretLevel"));
|
||||||
|
if (userSecretLevel == null || fileSecretLevel >= userSecretLevel) {
|
||||||
|
return Result.error("当前用户密级不足,无法预览文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filePath = Objects.toString(fileInfo.get("filePath"), "");
|
||||||
|
if (StringUtils.isEmpty(filePath)) {
|
||||||
|
return Result.error("文件路径为空,无法预览");
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
String fileName = Objects.toString(fileInfo.get("fileName"), getFileNameFromPath(filePath));
|
||||||
|
JSONObject previewInfo = new JSONObject();
|
||||||
|
previewInfo.put("filePath", filePath);
|
||||||
|
previewInfo.put("fileName", fileName);
|
||||||
|
previewInfo.put("fileId", id);
|
||||||
|
previewInfo.put("userId", sysUser.getId());
|
||||||
|
// kkFileView runs outside the browser session, so this token is the short-lived authorization.
|
||||||
|
redisUtil.set(FILE_PREVIEW_REDIS_PREFIX + token, previewInfo.toJSONString(), FILE_PREVIEW_TOKEN_EXPIRE_SECONDS);
|
||||||
|
|
||||||
|
String previewDownloadUrl = buildPreviewDownloadUrl(request, token, fileName);
|
||||||
|
return Result.OK(previewDownloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "附件预览解密流")
|
||||||
|
@GetMapping(value = "/previewDownload")
|
||||||
|
public void previewDownload(@RequestParam String token, HttpServletResponse response) throws Exception {
|
||||||
|
if (StringUtils.isEmpty(token)) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object cache = redisUtil.get(FILE_PREVIEW_REDIS_PREFIX + token);
|
||||||
|
if (cache == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject previewInfo = parsePreviewInfo(cache);
|
||||||
|
String filePath = previewInfo.getString("filePath");
|
||||||
|
String fileName = previewInfo.getString("fileName");
|
||||||
|
if (StringUtils.isEmpty(filePath)) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String bucketName = MinioUtil.getBucketName();
|
||||||
|
String objectName = normalizeMinioObjectName(filePath);
|
||||||
|
try (InputStream inputStream = MinioUtil.getMinioFile(bucketName, objectName)) {
|
||||||
|
if (inputStream == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] fileBytes = IOUtils.toByteArray(inputStream);
|
||||||
|
byte[] decryptedBytes = SecureUtils.decrypt(fileBytes);
|
||||||
|
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader("Content-Disposition", "inline;fileName=\"" + encodeFileName(fileName) + "\"");
|
||||||
|
response.setContentLength(decryptedBytes.length);
|
||||||
|
response.getOutputStream().write(decryptedBytes);
|
||||||
|
response.flushBuffer();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("附件预览解密失败: {}", filePath, e);
|
||||||
|
if (!response.isCommitted()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String securityLevel(String secLevel) {
|
private String securityLevel(String secLevel) {
|
||||||
if ("1".equals(secLevel)) {
|
if ("1".equals(secLevel)) {
|
||||||
return "非密";
|
return "非密";
|
||||||
@@ -504,4 +593,66 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
return pre+secLevelText+suffix;
|
return pre+secLevelText+suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int parseSecretLevel(Object secretLevel) {
|
||||||
|
if (secretLevel == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(secretLevel));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRequestBaseUrl(HttpServletRequest request) {
|
||||||
|
String scheme = request.getScheme();
|
||||||
|
int port = request.getServerPort();
|
||||||
|
StringBuilder url = new StringBuilder();
|
||||||
|
url.append(scheme).append("://").append(request.getServerName());
|
||||||
|
if (("http".equalsIgnoreCase(scheme) && port != 80) || ("https".equalsIgnoreCase(scheme) && port != 443)) {
|
||||||
|
url.append(":").append(port);
|
||||||
|
}
|
||||||
|
url.append(request.getContextPath());
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPreviewDownloadUrl(HttpServletRequest request, String token, String fileName) {
|
||||||
|
// kkFileView judges the preview handler by file suffix; stream APIs without suffix need fullfilename.
|
||||||
|
return buildRequestBaseUrl(request) + "/sys/file/previewDownload?token=" + token + "&fullfilename=" + encodeUrlParam(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject parsePreviewInfo(Object cache) {
|
||||||
|
if (cache instanceof JSONObject) {
|
||||||
|
return (JSONObject) cache;
|
||||||
|
}
|
||||||
|
return JSONObject.parseObject(String.valueOf(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeMinioObjectName(String filePath) {
|
||||||
|
String objectName = filePath;
|
||||||
|
while (objectName.startsWith("/")) {
|
||||||
|
objectName = objectName.substring(1);
|
||||||
|
}
|
||||||
|
return objectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileNameFromPath(String filePath) {
|
||||||
|
String objectName = normalizeMinioObjectName(filePath);
|
||||||
|
int index = objectName.lastIndexOf("/");
|
||||||
|
return index >= 0 ? objectName.substring(index + 1) : objectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeFileName(String fileName) throws Exception {
|
||||||
|
String name = StringUtils.isEmpty(fileName) ? "preview" : fileName;
|
||||||
|
return URLEncoder.encode(name, "UTF-8").replace("+", "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeUrlParam(String value) {
|
||||||
|
try {
|
||||||
|
return URLEncoder.encode(StringUtils.isEmpty(value) ? "preview" : value, "UTF-8");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "preview";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user