diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index 10a63eb4..0099bc54 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -159,8 +159,9 @@ watermark.height = ${WATERMARK_HEIGHT:80} #水印倾斜度数,要求设置在大于等于0,小于90 watermark.angle = ${WATERMARK_ANGLE:10} - #首页功能设置 +#是否禁用首页文件上传 +file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:true} # 备案信息,默认为空 beian = ${KK_BEIAN:default} #禁止上传类型 diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java index 432ea2dc..9310441c 100644 --- a/server/src/main/java/cn/keking/config/ConfigConstants.java +++ b/server/src/main/java/cn/keking/config/ConfigConstants.java @@ -426,7 +426,7 @@ public class ConfigConstants { return fileUploadDisable; } - @Value("${file.upload.disable:false}") + @Value("${file.upload.disable:true}") public void setFileUploadDisable(Boolean fileUploadDisable) { setFileUploadDisableValue(fileUploadDisable); } diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 6743b555..0ea6a914 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -53,6 +53,77 @@ public class FileController { private final String demoPath = demoDir + File.separator; public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!"; + @PostMapping("/fileUpload") + public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file) { + ReturnResponse checkResult = this.fileUploadCheck(file); + if (checkResult.isFailure()) { + return checkResult; + } + File outFile = new File(fileDir + demoPath); + if (!outFile.exists() && !outFile.mkdirs()) { + logger.error("创建文件夹【{}】失败,请检查目录权限!", fileDir + demoPath); + } + String fileName = checkResult.getContent().toString(); + logger.info("上传文件:{}{}{}", fileDir, demoPath, fileName); + try (InputStream in = file.getInputStream(); OutputStream out = Files.newOutputStream(Paths.get(fileDir + demoPath + fileName))) { + StreamUtils.copy(in, out); + return ReturnResponse.success(null); + } catch (IOException e) { + logger.error("文件上传失败", e); + return ReturnResponse.failure(); + } + } + + @GetMapping("/deleteFile") + public ReturnResponse deleteFile(HttpServletRequest request, String fileName, String password) { + ReturnResponse checkResult = this.deleteFileCheck(request, fileName, password); + if (checkResult.isFailure()) { + return checkResult; + } + fileName = checkResult.getContent().toString(); + File file = new File(fileDir + demoPath + fileName); + logger.info("删除文件:{}", file.getAbsolutePath()); + if (file.exists() && !file.delete()) { + String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath()); + logger.error(msg); + return ReturnResponse.failure(msg); + } + WebUtils.removeSessionAttr(request, CAPTCHA_CODE); //删除缓存验证码 + return ReturnResponse.success(); + } + + /** + * 验证码方法 + */ + @RequestMapping("/deleteFile/captcha") + public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (!ConfigConstants.getDeleteCaptcha()) { + return; + } + + response.setContentType("image/jpeg"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setDateHeader("Expires", -1); + String captchaCode = WebUtils.getSessionAttr(request, CAPTCHA_CODE); + long captchaGenerateTime = WebUtils.getLongSessionAttr(request, CAPTCHA_GENERATE_TIME); + long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime); + + // 验证码为空,且生成验证码超过50秒,重新生成验证码 + if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) { + captchaCode = CaptchaUtil.generateCaptchaCode(); + // 更新验证码 + WebUtils.setSessionAttr(request, CAPTCHA_CODE, captchaCode); + WebUtils.setSessionAttr(request, CAPTCHA_GENERATE_TIME, DateUtils.getCurrentSecond()); + } else { + captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode; + } + + ServletOutputStream outputStream = response.getOutputStream(); + ImageIO.write(CaptchaUtil.generateCaptchaPic(captchaCode), "jpeg", outputStream); + outputStream.close(); + } + @GetMapping("/listFiles") public List> getFiles() { List> list = new ArrayList<>(); @@ -69,6 +140,70 @@ public class FileController { return list; } + /** + * 上传文件前校验 + * + * @param file 文件 + * @return 校验结果 + */ + private ReturnResponse fileUploadCheck(MultipartFile file) { + if (ConfigConstants.getFileUploadDisable()) { + return ReturnResponse.failure("文件传接口已禁用"); + } + String fileName = WebUtils.getFileNameFromMultipartFile(file); + if (fileName.lastIndexOf(".") == -1) { + return ReturnResponse.failure("不允许上传的类型"); + } + if (!KkFileUtils.isAllowedUpload(fileName)) { + return ReturnResponse.failure("不允许上传的文件类型: " + fileName); + } + if (KkFileUtils.isIllegalFileName(fileName)) { + return ReturnResponse.failure("不允许上传的文件名: " + fileName); + } + // 判断是否存在同名文件 + if (existsFile(fileName)) { + return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传"); + } + return ReturnResponse.success(fileName); + } + + + /** + * 删除文件前校验 + * + * @param fileName 文件名 + * @return 校验结果 + */ + private ReturnResponse deleteFileCheck(HttpServletRequest request, String fileName, String password) { + if (ObjectUtils.isEmpty(fileName)) { + return ReturnResponse.failure("文件名为空,删除失败!"); + } + try { + fileName = WebUtils.decodeUrl(fileName); + } catch (Exception ex) { + String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName); + return ReturnResponse.failure(errorMsg + "删除失败!"); + } + assert fileName != null; + if (fileName.contains("/")) { + fileName = fileName.substring(fileName.lastIndexOf("/") + 1); + } + if (KkFileUtils.isIllegalFileName(fileName)) { + return ReturnResponse.failure("非法文件名,删除失败!"); + } + if (ObjectUtils.isEmpty(password)) { + return ReturnResponse.failure("密码 or 验证码为空,删除失败!"); + } + + String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, CAPTCHA_CODE) : ConfigConstants.getPassword(); + + if (!password.equalsIgnoreCase(expectedPassword)) { + logger.error("删除文件【{}】失败,密码错误!", fileName); + return ReturnResponse.failure("删除文件失败,密码错误!"); + } + return ReturnResponse.success(fileName); + } + @GetMapping("/directory") public Object directory(String urls) { String fileUrl; @@ -84,4 +219,9 @@ public class FileController { } return RarUtils.getTree(fileUrl); } + + private boolean existsFile(String fileName) { + File file = new File(fileDir + demoPath + fileName); + return file.exists(); + } } diff --git a/server/src/main/resources/web/main/index.ftl b/server/src/main/resources/web/main/index.ftl index c4ebb5f7..76d232e3 100644 --- a/server/src/main/resources/web/main/index.ftl +++ b/server/src/main/resources/web/main/index.ftl @@ -148,6 +148,18 @@ + <#else> +
+

+ 文件上传功能默认已禁用。如需开启,请通过以下方式配置: +
+ • 配置文件:file.upload.disable=false +
+ • 环境变量:KK_FILE_UPLOAD_DISABLE=false +
+ 请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。 +

+