From 7af4abea43c124a4f40eb54bdad33795b91067ed Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 00:18:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B6=E5=8F=91=E5=B9=82?= =?UTF-8?q?=E7=AD=89=E4=B8=8E=E8=B7=AF=E5=BE=84/CORS=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/test/demo/config/WebMvcConfig.java | 34 ++- .../interceptor/IdempotentInterceptor.java | 26 ++- .../test/demo/service/DynamicTaskService.java | 197 +++++++++++++----- .../demo/service/impl/AuthServiceImpl.java | 25 ++- .../demo/service/impl/UserServiceImpl.java | 31 ++- .../java/com/test/demo/util/FilePathUtil.java | 37 +++- src/main/resources/application.yml | 5 + 7 files changed, 259 insertions(+), 96 deletions(-) diff --git a/src/main/java/com/test/demo/config/WebMvcConfig.java b/src/main/java/com/test/demo/config/WebMvcConfig.java index 5104fbc..0d1971b 100644 --- a/src/main/java/com/test/demo/config/WebMvcConfig.java +++ b/src/main/java/com/test/demo/config/WebMvcConfig.java @@ -5,7 +5,10 @@ import cn.dev33.satoken.stp.StpUtil; import com.test.demo.interceptor.IdempotentInterceptor; import com.test.demo.interceptor.SignatureInterceptor; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -17,14 +20,27 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { - private final SignatureInterceptor signatureInterceptor; + private static final String[] LOCAL_DEV_ORIGINS = { + "http://localhost:3000", + "http://localhost:5173", + "http://127.0.0.1:3000", + "http://127.0.0.1:5173" + }; + private final SignatureInterceptor signatureInterceptor; private final IdempotentInterceptor idempotentInterceptor; + @Value("${app.cors.allowed-origins:}") + private String[] allowedOrigins; + @Override public void addCorsMappings(CorsRegistry registry) { + String[] corsOrigins = (allowedOrigins == null || allowedOrigins.length == 0 + || Arrays.stream(allowedOrigins).allMatch(String::isBlank)) + ? LOCAL_DEV_ORIGINS + : Arrays.stream(allowedOrigins).map(String::trim).filter(origin -> !origin.isBlank()).toArray(String[]::new); registry.addMapping("/**") - .allowedOriginPatterns("*") + .allowedOrigins(corsOrigins) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) @@ -39,14 +55,18 @@ public class WebMvcConfig implements WebMvcConfigurer { "/doc.html", "/webjars/**", "/v3/api-docs/**", - "/swagger-resources/**", - "/actuator/**" + "/swagger-resources/**" ); registry.addInterceptor(idempotentInterceptor) .addPathPatterns("/**"); - // Sa-Token 路由拦截 + registry.addInterceptor(new SaInterceptor(handle -> { + StpUtil.checkLogin(); + StpUtil.checkRole("ADMIN"); + })) + .addPathPatterns("/actuator/**"); + registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) .addPathPatterns("/**") .excludePathPatterns( @@ -55,11 +75,11 @@ public class WebMvcConfig implements WebMvcConfigurer { "/captcha/**", "/file/preview/**", "/enums/**", + "/actuator/**", "/doc.html", "/webjars/**", "/v3/api-docs/**", - "/swagger-resources/**", - "/actuator/**" + "/swagger-resources/**" ); } } diff --git a/src/main/java/com/test/demo/interceptor/IdempotentInterceptor.java b/src/main/java/com/test/demo/interceptor/IdempotentInterceptor.java index 1fb90ca..aee4008 100644 --- a/src/main/java/com/test/demo/interceptor/IdempotentInterceptor.java +++ b/src/main/java/com/test/demo/interceptor/IdempotentInterceptor.java @@ -6,6 +6,7 @@ import com.test.demo.common.constant.CommonConstant; import com.test.demo.common.constant.RedisKeyConstant; import com.test.demo.common.exception.BizException; import com.test.demo.common.result.ResultCode; +import com.test.demo.filter.RepeatableReadRequestWrapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -15,6 +16,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; /** @@ -38,14 +40,26 @@ public class IdempotentInterceptor implements HandlerInterceptor { return true; } - // 构建幂等 key: token + url + 参数hash String token = request.getHeader(CommonConstant.HEADER_IDEMPOTENT_TOKEN); - if (token == null) { + if (token == null || token.isBlank()) { token = request.getHeader("Authorization"); } String url = request.getRequestURI(); String queryString = request.getQueryString() != null ? request.getQueryString() : ""; - String key = RedisKeyConstant.IDEMPOTENT + DigestUtil.md5Hex(token + url + queryString); + String body = ""; + if (request instanceof RepeatableReadRequestWrapper wrapper) { + body = new String(wrapper.getBody(), StandardCharsets.UTF_8); + } + String bodyHash = DigestUtil.sha256Hex(body); + String tokenPart; + if (token == null || token.isBlank()) { + String ip = firstNonBlank(request.getHeader("X-Forwarded-For"), request.getRemoteAddr()); + String userAgent = firstNonBlank(request.getHeader("User-Agent"), "unknown"); + tokenPart = request.getMethod() + ":anonymous:" + ip + ":" + userAgent + ":" + url + ":" + queryString + ":" + bodyHash; + } else { + tokenPart = token; + } + String key = RedisKeyConstant.IDEMPOTENT + DigestUtil.md5Hex(tokenPart + url + queryString + bodyHash); Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, "1", idempotent.expireTime(), TimeUnit.SECONDS); if (Boolean.FALSE.equals(absent)) { @@ -55,4 +69,10 @@ public class IdempotentInterceptor implements HandlerInterceptor { return true; } + private String firstNonBlank(String first, String second) { + if (first != null && !first.isBlank()) { + return first.split(",")[0].trim(); + } + return second == null ? "" : second; + } } diff --git a/src/main/java/com/test/demo/service/DynamicTaskService.java b/src/main/java/com/test/demo/service/DynamicTaskService.java index 14bb398..2ec73d6 100644 --- a/src/main/java/com/test/demo/service/DynamicTaskService.java +++ b/src/main/java/com/test/demo/service/DynamicTaskService.java @@ -1,5 +1,6 @@ package com.test.demo.service; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.test.demo.bo.TaskQueryBo; @@ -12,15 +13,16 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; -import org.springframework.context.ApplicationContext; import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronExpression; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; -import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; /** * 动态定时任务服务 @@ -30,103 +32,147 @@ import java.util.concurrent.ScheduledFuture; @RequiredArgsConstructor public class DynamicTaskService implements ApplicationRunner { + private static final String TEMPLATE_BEAN_NAME = "dynamicTaskService"; + private static final String TEMPLATE_METHOD_HEARTBEAT = "logHeartbeat"; + private static final String TEMPLATE_METHOD_LOG_MESSAGE = "logMessage"; + private final ScheduledTaskMapper taskMapper; private final TaskScheduler taskScheduler; - private final ApplicationContext applicationContext; private final Map> runningTasks = new ConcurrentHashMap<>(); + private final Map taskLocks = new ConcurrentHashMap<>(); + private final Map> taskTemplates = Map.of( + templateKey(TEMPLATE_BEAN_NAME, TEMPLATE_METHOD_HEARTBEAT), params -> log.info("执行白名单任务模板: heartbeat"), + templateKey(TEMPLATE_BEAN_NAME, TEMPLATE_METHOD_LOG_MESSAGE), + params -> log.info("执行白名单任务模板: logMessage, message={}", StrUtil.blankToDefault(params, "")) + ); @Override public void run(ApplicationArguments args) { - // 启动时加载所有运行中的任务 var tasks = taskMapper.selectList( new LambdaQueryWrapper().eq(ScheduledTaskEntity::getStatus, 1)); for (ScheduledTaskEntity task : tasks) { + validateTaskTemplate(task); addTask(task); } log.info("动态定时任务加载完成, 共{}个", tasks.size()); } - /** 添加并启动任务 */ public void addTask(ScheduledTaskEntity task) { - ScheduledFuture future = taskScheduler.schedule( - () -> executeTask(task), new CronTrigger(task.getCronExpression())); - runningTasks.put(task.getId(), future); - log.info("定时任务已启动: {} [{}]", task.getTaskName(), task.getCronExpression()); + validateTaskTemplate(task); + withTaskLock(task.getId(), () -> { + if (isFutureActive(runningTasks.get(task.getId()))) { + log.warn("定时任务已在运行,跳过重复启动: taskId={}", task.getId()); + return; + } + ScheduledFuture future = taskScheduler.schedule( + () -> executeTask(task), new CronTrigger(task.getCronExpression())); + if (future == null) { + throw new BizException(ResultCode.INTERNAL_ERROR, "定时任务启动失败"); + } + runningTasks.put(task.getId(), future); + log.info("定时任务已启动: {} [{}]", task.getTaskName(), task.getCronExpression()); + }); } - /** 停止任务 */ public void removeTask(Long taskId) { - ScheduledFuture future = runningTasks.remove(taskId); - if (future != null) { - future.cancel(false); - log.info("定时任务已停止: taskId={}", taskId); - } + withTaskLock(taskId, () -> doRemoveTask(taskId)); } - /** 重启任务 */ public void restartTask(ScheduledTaskEntity task) { - removeTask(task.getId()); - addTask(task); + validateTaskTemplate(task); + withTaskLock(task.getId(), () -> { + doRemoveTask(task.getId()); + ScheduledFuture future = taskScheduler.schedule( + () -> executeTask(task), new CronTrigger(task.getCronExpression())); + if (future == null) { + throw new BizException(ResultCode.INTERNAL_ERROR, "定时任务重启失败"); + } + runningTasks.put(task.getId(), future); + log.info("定时任务已重启: {} [{}]", task.getTaskName(), task.getCronExpression()); + }); } private void executeTask(ScheduledTaskEntity task) { try { - Object bean = applicationContext.getBean(task.getBeanName()); - Method method; - if (task.getMethodParams() != null && !task.getMethodParams().isEmpty()) { - method = bean.getClass().getMethod(task.getMethodName(), String.class); - method.invoke(bean, task.getMethodParams()); - } else { - method = bean.getClass().getMethod(task.getMethodName()); - method.invoke(bean); + Consumer taskExecutor = taskTemplates.get(templateKey(task.getBeanName(), task.getMethodName())); + if (taskExecutor == null) { + throw new BizException(ResultCode.FORBIDDEN, "任务模板未授权"); } + taskExecutor.accept(task.getMethodParams()); } catch (Exception e) { log.error("定时任务执行失败: {}", task.getTaskName(), e); } } - /** 创建任务 */ public void create(ScheduledTaskEntity task) { + normalizeAndValidateTask(task); task.setStatus(1); taskMapper.insert(task); addTask(task); } - /** 更新任务 */ public void update(ScheduledTaskEntity task) { + ScheduledTaskEntity existing = requireTask(task.getId()); + normalizeAndValidateTask(task); + task.setStatus(task.getStatus() == null ? existing.getStatus() : task.getStatus()); taskMapper.updateById(task); - if (task.getStatus() != null && task.getStatus() == 1) { - restartTask(task); + ScheduledTaskEntity latest = requireTask(task.getId()); + if (Integer.valueOf(1).equals(latest.getStatus())) { + restartTask(latest); } else { removeTask(task.getId()); } } - /** 暂停任务 */ public void pause(Long taskId) { - ScheduledTaskEntity entity = new ScheduledTaskEntity(); - entity.setId(taskId); - entity.setStatus(0); - taskMapper.updateById(entity); - removeTask(taskId); + withTaskLock(taskId, () -> { + ScheduledTaskEntity task = requireTask(taskId); + if (Integer.valueOf(0).equals(task.getStatus())) { + doRemoveTask(taskId); + return; + } + ScheduledTaskEntity entity = new ScheduledTaskEntity(); + entity.setId(taskId); + entity.setStatus(0); + taskMapper.updateById(entity); + doRemoveTask(taskId); + }); } - /** 恢复任务 */ public void resume(Long taskId) { - ScheduledTaskEntity task = taskMapper.selectById(taskId); - if (task == null) { - throw new BizException(ResultCode.NOT_FOUND, "任务不存在"); - } - task.setStatus(1); - taskMapper.updateById(task); - addTask(task); + withTaskLock(taskId, () -> { + ScheduledTaskEntity task = requireTask(taskId); + validateTaskTemplate(task); + if (Integer.valueOf(1).equals(task.getStatus()) && isFutureActive(runningTasks.get(taskId))) { + log.warn("定时任务已恢复,跳过重复调度: taskId={}", taskId); + return; + } + if (!Integer.valueOf(1).equals(task.getStatus())) { + ScheduledTaskEntity entity = new ScheduledTaskEntity(); + entity.setId(taskId); + entity.setStatus(1); + taskMapper.updateById(entity); + task.setStatus(1); + } + ScheduledFuture future = taskScheduler.schedule( + () -> executeTask(task), new CronTrigger(task.getCronExpression())); + if (future == null) { + throw new BizException(ResultCode.INTERNAL_ERROR, "定时任务恢复失败"); + } + ScheduledFuture previous = runningTasks.put(taskId, future); + if (previous != null && previous != future) { + previous.cancel(false); + } + log.info("定时任务已启动: {} [{}]", task.getTaskName(), task.getCronExpression()); + }); } - /** 删除任务 */ public void delete(Long taskId) { + requireTask(taskId); removeTask(taskId); taskMapper.deleteById(taskId); + taskLocks.remove(taskId); } public PageResult page(TaskQueryBo query) { @@ -140,10 +186,65 @@ public class DynamicTaskService implements ApplicationRunner { } public ScheduledTaskEntity detail(Long id) { - ScheduledTaskEntity entity = taskMapper.selectById(id); - if (entity == null) { + return requireTask(id); + } + + private void normalizeAndValidateTask(ScheduledTaskEntity task) { + if (task == null) { + throw new BizException(ResultCode.BAD_REQUEST, "任务不能为空"); + } + if (!CronExpression.isValidExpression(task.getCronExpression())) { + throw new BizException(ResultCode.BAD_REQUEST, "Cron 表达式非法"); + } + validateTaskTemplate(task); + } + + private void validateTaskTemplate(ScheduledTaskEntity task) { + if (task == null || StrUtil.isBlank(task.getBeanName()) || StrUtil.isBlank(task.getMethodName())) { + throw new BizException(ResultCode.BAD_REQUEST, "任务模板不能为空"); + } + String key = templateKey(task.getBeanName(), task.getMethodName()); + if (!taskTemplates.containsKey(key)) { + throw new BizException(ResultCode.FORBIDDEN, + "仅允许执行白名单任务模板,当前支持: dynamicTaskService.logHeartbeat / dynamicTaskService.logMessage"); + } + } + + private ScheduledTaskEntity requireTask(Long taskId) { + ScheduledTaskEntity task = taskMapper.selectById(taskId); + if (task == null) { throw new BizException(ResultCode.NOT_FOUND, "任务不存在"); } - return entity; + return task; + } + + private void doRemoveTask(Long taskId) { + ScheduledFuture future = runningTasks.remove(taskId); + if (future != null) { + future.cancel(false); + log.info("定时任务已停止: taskId={}", taskId); + } + } + + private boolean isFutureActive(ScheduledFuture future) { + return future != null && !future.isCancelled() && !future.isDone(); + } + + private void withTaskLock(Long taskId, Runnable action) { + if (taskId == null) { + action.run(); + return; + } + ReentrantLock lock = taskLocks.computeIfAbsent(taskId, key -> new ReentrantLock()); + lock.lock(); + try { + action.run(); + } finally { + lock.unlock(); + } + } + + private static String templateKey(String beanName, String methodName) { + return beanName + "#" + methodName; } } diff --git a/src/main/java/com/test/demo/service/impl/AuthServiceImpl.java b/src/main/java/com/test/demo/service/impl/AuthServiceImpl.java index 81388b6..46852ce 100644 --- a/src/main/java/com/test/demo/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/test/demo/service/impl/AuthServiceImpl.java @@ -21,6 +21,8 @@ import com.test.demo.service.UserService; import com.test.demo.vo.LoginVo; import com.test.demo.vo.UserVo; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -34,7 +36,6 @@ import java.util.concurrent.TimeUnit; public class AuthServiceImpl implements AuthService { private static final long LOGIN_FAIL_EXPIRE_MINUTES = 15; - private static final long LOGIN_FAIL_MAX_TIMES = 5; private final UserService userService; @@ -94,11 +95,6 @@ public class AuthServiceImpl implements AuthService { if (!captchaService.verify(bo.getCaptchaKey(), bo.getCaptchaCode())) { throw new BizException(ResultCode.BAD_REQUEST, "验证码错误或已过期"); } - long count = userService.count(new LambdaQueryWrapper() - .eq(UserEntity::getUsername, bo.getUsername())); - if (count > 0) { - throw new BizException(ResultCode.CONFLICT, "用户名已存在"); - } UserEntity user = new UserEntity(); user.setUsername(bo.getUsername()); user.setNickname(bo.getNickname()); @@ -109,7 +105,11 @@ public class AuthServiceImpl implements AuthService { user.setSalt(salt); user.setPassword(SM3Util.hash(bo.getPassword(), salt)); user.setStatus(UserStatusEnum.NORMAL); - userService.save(user); + try { + userService.save(user); + } catch (DuplicateKeyException | DataIntegrityViolationException ex) { + throw new BizException(ResultCode.CONFLICT, "用户名已存在"); + } } @Override @@ -139,14 +139,17 @@ public class AuthServiceImpl implements AuthService { private long getLoginFailTimes(String key) { Object value = redisTemplate.opsForValue().get(key); if (value == null) { - return 0L; + return 0; } - return Long.parseLong(value.toString()); + if (value instanceof Number number) { + return number.longValue(); + } + return Long.parseLong(String.valueOf(value)); } private void increaseLoginFailTimes(String key) { - Long count = redisTemplate.opsForValue().increment(key); - if (count != null && count == 1L) { + Long failTimes = redisTemplate.opsForValue().increment(key); + if (failTimes != null && failTimes == 1L) { redisTemplate.expire(key, LOGIN_FAIL_EXPIRE_MINUTES, TimeUnit.MINUTES); } } diff --git a/src/main/java/com/test/demo/service/impl/UserServiceImpl.java b/src/main/java/com/test/demo/service/impl/UserServiceImpl.java index 92e87b1..3b5c917 100644 --- a/src/main/java/com/test/demo/service/impl/UserServiceImpl.java +++ b/src/main/java/com/test/demo/service/impl/UserServiceImpl.java @@ -14,12 +14,15 @@ import com.test.demo.entity.UserEntity; import com.test.demo.enums.UserStatusEnum; import com.test.demo.mapper.UserMapper; import com.test.demo.security.crypto.SM3Util; +import com.test.demo.service.UserService; import com.test.demo.util.ExcelUtil; +import com.test.demo.util.PasswordPolicyUtil; import com.test.demo.vo.UserImportVo; import com.test.demo.vo.UserVo; -import com.test.demo.service.UserService; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -62,20 +65,16 @@ public class UserServiceImpl extends ServiceImpl impleme @Override public void add(UserBo bo) { - // 检查用户名唯一 - long count = count(new LambdaQueryWrapper() - .eq(UserEntity::getUsername, bo.getUsername())); - if (count > 0) { - throw new BizException(ResultCode.CONFLICT, "用户名已存在"); - } - UserEntity entity = userConvert.toEntity(bo); - // SM3加密密码 String salt = SM3Util.generateSalt(); entity.setSalt(salt); entity.setPassword(SM3Util.hash(bo.getPassword(), salt)); entity.setStatus(UserStatusEnum.NORMAL); - save(entity); + try { + save(entity); + } catch (DuplicateKeyException | DataIntegrityViolationException ex) { + throw new BizException(ResultCode.CONFLICT, "用户名已存在"); + } } @Override @@ -98,6 +97,7 @@ public class UserServiceImpl extends ServiceImpl impleme @Override public void resetPassword(Long id, String newPassword) { + PasswordPolicyUtil.validate(newPassword); UserEntity entity = getById(id); if (entity == null) { throw new BizException(ResultCode.NOT_FOUND, "用户不存在"); @@ -140,11 +140,6 @@ public class UserServiceImpl extends ServiceImpl impleme if (StrUtil.isBlank(row.getUsername())) { continue; } - long count = count(new LambdaQueryWrapper() - .eq(UserEntity::getUsername, row.getUsername())); - if (count > 0) { - continue; - } UserEntity entity = new UserEntity(); entity.setUsername(row.getUsername()); entity.setNickname(row.getNickname()); @@ -156,7 +151,11 @@ public class UserServiceImpl extends ServiceImpl impleme entity.setSalt(salt); entity.setPassword(SM3Util.hash(rawPassword, salt)); entity.setStatus(UserStatusEnum.NORMAL); - save(entity); + try { + save(entity); + } catch (DuplicateKeyException | DataIntegrityViolationException ex) { + throw new BizException(ResultCode.CONFLICT, "导入失败,用户名已存在: " + row.getUsername()); + } } } catch (IOException e) { throw new BizException(ResultCode.INTERNAL_ERROR, "导入文件读取失败"); diff --git a/src/main/java/com/test/demo/util/FilePathUtil.java b/src/main/java/com/test/demo/util/FilePathUtil.java index bf4d94a..e19bfc4 100644 --- a/src/main/java/com/test/demo/util/FilePathUtil.java +++ b/src/main/java/com/test/demo/util/FilePathUtil.java @@ -2,6 +2,12 @@ package com.test.demo.util; import cn.hutool.core.util.StrUtil; +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; + /** * 文件路径安全校验工具 */ @@ -16,22 +22,31 @@ public final class FilePathUtil { * @return 是否安全 */ public static boolean isSafePath(String basePath, String filePath) { - if (StrUtil.isBlank(filePath)) { + if (StrUtil.isBlank(basePath) || StrUtil.isBlank(filePath)) { return false; } - // 禁止路径穿越 - if (filePath.contains("..") || filePath.contains("./")) { + try { + Path normalizedBasePath = Paths.get(basePath).toAbsolutePath().normalize(); + Path normalizedResolvedPath = normalizedBasePath.resolve(filePath).normalize(); + Path realBasePath = toRealPathIfPossible(normalizedBasePath); + Path realResolvedPath = toRealPathIfPossible(normalizedResolvedPath); + return realResolvedPath.startsWith(realBasePath); + } catch (InvalidPathException | IOException ex) { return false; } - // 禁止绝对路径 - if (filePath.startsWith("/") || filePath.contains(":")) { - return false; + } + + private static Path toRealPathIfPossible(Path path) throws IOException { + try { + return path.toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException ex) { + Path parent = path.getParent(); + if (parent == null) { + throw ex; + } + Path realParent = parent.toRealPath(LinkOption.NOFOLLOW_LINKS); + return realParent.resolve(path.getFileName()).normalize(); } - // 禁止特殊字符 - if (filePath.matches(".*[\\\\<>|\"?*].*")) { - return false; - } - return true; } /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 092d9da..5103a18 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -84,3 +84,8 @@ management: show-details: when-authorized mail: enabled: false + + +app: + cors: + allowed-origins: ${APP_CORS_ALLOWED_ORIGINS:}