From 35feb9a14754b29e67b1c87cf695cc1c80105472 Mon Sep 17 00:00:00 2001 From: sh Date: Sat, 2 May 2026 20:41:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=BC=8F=E6=B4=9E=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/system/SysLoginController.java | 67 ++++++++++++++- .../cms/controller/app/AppFileController.java | 57 +++++++++++-- .../cms/service/impl/FileServiceImpl.java | 11 ++- .../com/ruoyi/cms/util/file/FileValid.java | 84 +++++++++++++++++++ .../common/constant/EncryptConstants.java | 15 +++- .../common/filter/EncryptResponseFilter.java | 7 ++ .../web/service/SysLoginService.java | 4 +- 7 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/file/FileValid.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index 557e895..3926274 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -2,16 +2,23 @@ package com.ruoyi.web.controller.system; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import com.ruoyi.cms.util.StringUtil; +import com.ruoyi.common.core.domain.entity.AppUser; import com.ruoyi.common.core.domain.entity.tymh.wwToken.WwTokenResult; import com.ruoyi.common.core.domain.entity.tymh.wwToken.WwUserLogin; import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.SiteSecurityUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.framework.web.service.OauthLoginHlwService; import com.ruoyi.framework.web.service.OauthLoginService; import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.AjaxResult; @@ -22,8 +29,11 @@ import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.web.service.SysLoginService; import com.ruoyi.framework.web.service.SysPermissionService; import com.ruoyi.system.service.ISysMenuService; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; /** @@ -46,6 +56,8 @@ public class SysLoginController private OauthLoginService oauthLoginService; @Autowired private OauthLoginHlwService oauthLoginHlwService; + @Autowired + private RedisCache redisCache; /** * 登录方法 @@ -229,12 +241,43 @@ public class SysLoginController */ @ApiOperation("保存注册信息") @PostMapping("/registerUser") - public AjaxResult registerUser(@RequestBody RegisterBody registerBody) + public AjaxResult registerUser(@RequestBody RegisterBody registerBody,HttpServletRequest request) { + //限流 + if (!checkLimit(request)) { + return AjaxResult.error("请求过于频繁,请稍后再试"); + } + String token=loginService.registerAppUser(registerBody); return AjaxResult.success().put("token",token); } + /** + * 限流 + * @return + */ + public boolean checkLimit(HttpServletRequest request) { + String ip = IpUtils.getIpAddr(request);; + Long userId = SiteSecurityUtils.getUserId(); + + //用户限流 + String userKey = "limit:registerUser:uid:" + userId; + Long userCnt = redisCache.increment(userKey); + if (userCnt == null || userCnt > 1) { + return false; + } + redisCache.expire(userKey, 60, TimeUnit.SECONDS); + // IP 限流 + String ipKey = "limit:registerUser:ip:" + ip; + Long ipCnt = redisCache.increment(ipKey); + if (ipCnt == null || ipCnt > 3) { + return false; + } + redisCache.expire(ipKey, 60, TimeUnit.SECONDS); + + return true; + } + /** * 获取统一门户token */ @@ -285,4 +328,26 @@ public class SysLoginController } } + /** + * 退出 + * @return + */ + @PostMapping("/logout") + public AjaxResult logout() { + try { + SecurityContextHolder.clearContext(); + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpSession session = attributes.getRequest().getSession(false); + if (session != null) { + session.invalidate(); // 服务器端直接销毁 + } + } + return AjaxResult.success("退出成功,登录已失效"); + } catch (Exception e) { + return AjaxResult.error("退出失败"); + } + } + } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppFileController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppFileController.java index f3778bc..18f4de3 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppFileController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppFileController.java @@ -2,12 +2,14 @@ package com.ruoyi.cms.controller.app; import com.ruoyi.cms.util.IdGenerator; import com.ruoyi.cms.util.ProxyServerUtil; +import com.ruoyi.cms.util.file.FileValid; import com.ruoyi.common.core.domain.entity.File; import com.ruoyi.cms.service.IFileService; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.SiteSecurityUtils; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +17,8 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; import java.util.List; @RestController @@ -25,19 +29,54 @@ public class AppFileController extends BaseController { @Autowired private IdGenerator idGenerator; + //文件大小限制 10MB + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; + @ApiOperation("上传文件") @PostMapping("/upload") public AjaxResult upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "bussinessid",required = false) Long bussinessId) { - if(!(SiteSecurityUtils.isLogin() || SecurityUtils.isLogin())){ + if (file.isEmpty()) return AjaxResult.error("文件不能为空!"); + if (file.getSize() > MAX_FILE_SIZE) { + return AjaxResult.error("文件大小超出限制!"); + } + if(!(SiteSecurityUtils.isLogin() || SecurityUtils.isLogin())){ return AjaxResult.error("未登录,请登录后在上传文件!"); - } + } + boolean isValid = FileValid.validateFile(file); + if (!isValid) { + return AjaxResult.error("文件类型不合法或内容被篡改!"); + } return fileService.upload(file,bussinessId); } @ApiOperation("获取附件列表") @GetMapping("/list") - public TableDataInfo list(File file) + public TableDataInfo list(File file,HttpServletRequest request) { + String host = request.getHeader("Host"); + List allowedHosts = Arrays.asList( + "localhost", + "127.0.0.1", + "10.98.80.146", + "10.98.80.50", + "www.xjksly.cn", + "ks.zhaopinzao8dian.com" + ); + + if (host != null) { + if (host.contains(":")) { + host = host.split(":")[0]; + } + if (!allowedHosts.contains(host)) { + return error(401,"非法请求来源"); + } + } + + //修复CORS 跨域漏洞 + HttpServletResponse response = ServletUtils.getResponse(); + response.setHeader("Access-Control-Allow-Origin", ""); + response.setHeader("Access-Control-Allow-Credentials", ""); + if(file.getBussinessid()==null){ return error(400,"无效的业务id!"); } @@ -55,11 +94,19 @@ public class AppFileController extends BaseController { @ApiOperation("上传文件") @PostMapping("/uploadFile") public AjaxResult uploadFile(@RequestParam("file") MultipartFile file, @RequestParam(value = "bussinessid",required = false) Long bussinessId, HttpServletRequest request) { - String proxyServer = ProxyServerUtil.getProxyServer(request); - System.out.println("获取服务器地址======================"+proxyServer); + if (file.isEmpty()) return AjaxResult.error("文件不能为空!"); + if (file.getSize() > MAX_FILE_SIZE) { + return AjaxResult.error("文件大小超出限制!"); + } if(!(SiteSecurityUtils.isLogin() || SecurityUtils.isLogin())){ return AjaxResult.error("未登录,请登录后在上传文件!"); } + boolean isValid = FileValid.validateFile(file); + if (!isValid) { + return AjaxResult.error("文件类型不合法或内容被篡改!"); + } + String proxyServer = ProxyServerUtil.getProxyServer(request); + System.out.println("获取服务器地址======================"+proxyServer); if(bussinessId==null){ bussinessId=idGenerator.generateId(); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/FileServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/FileServiceImpl.java index 4218807..430d517 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/FileServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/FileServiceImpl.java @@ -117,10 +117,14 @@ public class FileServiceImpl extends ServiceImpl implements IF throw new SecurityException("非法路径,禁止访问"); } - // 保存文件到服务器 Files.copy(file.getInputStream(), filePath); + java.io.File destFile = filePath.toFile(); + destFile.setReadable(true, false); // 所有用户可读 + destFile.setWritable(false); // 禁止写入 + destFile.setExecutable(false); + // 保存文件信息到数据库 saveFileInfo(fileName, bussinessid); @@ -163,6 +167,11 @@ public class FileServiceImpl extends ServiceImpl implements IF // 保存文件到服务器 Files.copy(file.getInputStream(), filePath); + java.io.File destFile = filePath.toFile(); + destFile.setReadable(true, false); // 所有用户可读 + destFile.setWritable(false); // 禁止写入 + destFile.setExecutable(false); + // 保存文件信息到数据库 File svFile=saveFileInfo(fileName, bussinessid); AjaxResult ajaxResult=AjaxResult.success(); diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/file/FileValid.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/file/FileValid.java new file mode 100644 index 0000000..bcf9935 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/file/FileValid.java @@ -0,0 +1,84 @@ +package com.ruoyi.cms.util.file; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.List; + +/** + * 文件校验 + */ +public class FileValid { + + /** + * 白名单配置 + */ + final static List allowedExt = Arrays.asList( + "jpg", "jpeg", "png", "gif", "pdf", + "doc", "docx", "wps", + "xls", "xlsx", + "ppt", "pptx" + ); + + /** + * 支持的MIME类型 + */ + final static List allowedMime = Arrays.asList( + "image/jpeg", "image/png", "image/gif", "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-works", // wps + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" + ); + + /** + * 魔数校验 + */ + final static List allowedMagic = Arrays.asList( + "ffd8ffe0", "ffd8ffe1", // jpg + "89504e47", // png + "47494638", // gif + "25504446" // pdf + ); + + /** + * 文件类型 + MIME + 魔数 三重验证 + * @param file + * @return + */ + public static boolean validateFile(MultipartFile file) { + try { + // 白名单配置 + + String originalFilename = file.getOriginalFilename().toLowerCase(); + String contentType = file.getContentType(); + String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + + // 扩展名 + MIME 校验 + if (!allowedExt.contains(ext) || !allowedMime.contains(contentType)) { + return false; + } + + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 字节转16进制(魔数校验用) + * @param bytes + * @return + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/EncryptConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/EncryptConstants.java index 6f71c4d..6853605 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/EncryptConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/EncryptConstants.java @@ -52,6 +52,19 @@ public class EncryptConstants { "/cms/appUser/getUserInfo", "/cms/job/getAppUserYhsc", "/app/idCardLogin", - "/app/phoneLogin" + "/app/phoneLogin", + "/getInfo", + "/registerUser" + ); + + /** + * 不需要加密的返回URL路径模式 + */ + public static final List NOT_URL_PATTERNS = Arrays.asList( + "/app/user/list", + "/app/appskill/list", + "/app/userworkexperiences/list", + "/app/company/list", + "/app/companycontact/list" ); } \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseFilter.java index 0f41ca5..255e6eb 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseFilter.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/EncryptResponseFilter.java @@ -84,6 +84,13 @@ public class EncryptResponseFilter implements Filter { private boolean needProcess(HttpServletRequest request) { String requestURI = request.getRequestURI(); + //不加密返回 + for (String pattern : EncryptConstants.NOT_URL_PATTERNS) { + if (pathMatcher.match(pattern, requestURI)) { + return false; + } + } + // 检查URL是否匹配需要加解密的模式 for (String pattern : EncryptConstants.URL_PATTERNS) { if (pathMatcher.match(pattern, requestURI)) { diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index e52023e..547b86a 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -173,8 +173,8 @@ public class SysLoginService recordLoginInfo(appUser); AsyncManager.me().execute(AsyncFactory.recordLogininfor(appUser.getName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); recordLoginInfo(appUser); - // 生成token //tokenSiteService.createToken(loginSiteUser) 有时间 - return tokenSiteService.noExpireCreateToken(loginSiteUser); + // 生成token //tokenSiteService.noExpireCreateToken(loginSiteUser); + return tokenSiteService.createToken(loginSiteUser); } //单点登录