yxk-20260709
文件在线预览功能
This commit is contained in:
@@ -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
|
||||
|
||||
+152
-1
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user