yxk-20260709

文件在线预览功能
This commit is contained in:
ye1023
2026-07-09 10:18:56 +08:00
parent 2b43488245
commit 4bb9e82bad
2 changed files with 153 additions and 1 deletions
@@ -256,6 +256,7 @@ public class ShiroConfig {
filterMap.put("jwt", new JwtFilter(cloudServer==null));
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/sys/file/previewDownload", "anon");//kkFileView short-lived preview stream
filterChainDefinitionMap.put("/**", "jwt");
// 未授权界面返回JSON
@@ -59,6 +59,9 @@ public class FileController {
@Value(value = "${jeecg.appName}")
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) {
if ("1".equals(secLevel)) {
return "非密";
@@ -504,4 +593,66 @@ public class FileController {
}
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";
}
}
}