修复并发幂等与路径/CORS安全问题

This commit is contained in:
root
2026-03-17 00:18:09 +08:00
parent 2200009c8c
commit 7af4abea43
7 changed files with 259 additions and 96 deletions

View File

@@ -5,7 +5,10 @@ import cn.dev33.satoken.stp.StpUtil;
import com.test.demo.interceptor.IdempotentInterceptor; import com.test.demo.interceptor.IdempotentInterceptor;
import com.test.demo.interceptor.SignatureInterceptor; import com.test.demo.interceptor.SignatureInterceptor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; 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.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -17,14 +20,27 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor @RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer { 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; private final IdempotentInterceptor idempotentInterceptor;
@Value("${app.cors.allowed-origins:}")
private String[] allowedOrigins;
@Override @Override
public void addCorsMappings(CorsRegistry registry) { 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("/**") registry.addMapping("/**")
.allowedOriginPatterns("*") .allowedOrigins(corsOrigins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") .allowedHeaders("*")
.allowCredentials(true) .allowCredentials(true)
@@ -39,14 +55,18 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/doc.html", "/doc.html",
"/webjars/**", "/webjars/**",
"/v3/api-docs/**", "/v3/api-docs/**",
"/swagger-resources/**", "/swagger-resources/**"
"/actuator/**"
); );
registry.addInterceptor(idempotentInterceptor) registry.addInterceptor(idempotentInterceptor)
.addPathPatterns("/**"); .addPathPatterns("/**");
// Sa-Token 路由拦截 registry.addInterceptor(new SaInterceptor(handle -> {
StpUtil.checkLogin();
StpUtil.checkRole("ADMIN");
}))
.addPathPatterns("/actuator/**");
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**") .addPathPatterns("/**")
.excludePathPatterns( .excludePathPatterns(
@@ -55,11 +75,11 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/captcha/**", "/captcha/**",
"/file/preview/**", "/file/preview/**",
"/enums/**", "/enums/**",
"/actuator/**",
"/doc.html", "/doc.html",
"/webjars/**", "/webjars/**",
"/v3/api-docs/**", "/v3/api-docs/**",
"/swagger-resources/**", "/swagger-resources/**"
"/actuator/**"
); );
} }
} }

View File

@@ -6,6 +6,7 @@ import com.test.demo.common.constant.CommonConstant;
import com.test.demo.common.constant.RedisKeyConstant; import com.test.demo.common.constant.RedisKeyConstant;
import com.test.demo.common.exception.BizException; import com.test.demo.common.exception.BizException;
import com.test.demo.common.result.ResultCode; import com.test.demo.common.result.ResultCode;
import com.test.demo.filter.RepeatableReadRequestWrapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -15,6 +16,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@@ -38,14 +40,26 @@ public class IdempotentInterceptor implements HandlerInterceptor {
return true; return true;
} }
// 构建幂等 key: token + url + 参数hash
String token = request.getHeader(CommonConstant.HEADER_IDEMPOTENT_TOKEN); String token = request.getHeader(CommonConstant.HEADER_IDEMPOTENT_TOKEN);
if (token == null) { if (token == null || token.isBlank()) {
token = request.getHeader("Authorization"); token = request.getHeader("Authorization");
} }
String url = request.getRequestURI(); String url = request.getRequestURI();
String queryString = request.getQueryString() != null ? request.getQueryString() : ""; 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); Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, "1", idempotent.expireTime(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(absent)) { if (Boolean.FALSE.equals(absent)) {
@@ -55,4 +69,10 @@ public class IdempotentInterceptor implements HandlerInterceptor {
return true; return true;
} }
private String firstNonBlank(String first, String second) {
if (first != null && !first.isBlank()) {
return first.split(",")[0].trim();
}
return second == null ? "" : second;
}
} }

View File

@@ -1,5 +1,6 @@
package com.test.demo.service; package com.test.demo.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.test.demo.bo.TaskQueryBo; import com.test.demo.bo.TaskQueryBo;
@@ -12,15 +13,16 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture; 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 @RequiredArgsConstructor
public class DynamicTaskService implements ApplicationRunner { 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 ScheduledTaskMapper taskMapper;
private final TaskScheduler taskScheduler; private final TaskScheduler taskScheduler;
private final ApplicationContext applicationContext;
private final Map<Long, ScheduledFuture<?>> runningTasks = new ConcurrentHashMap<>(); private final Map<Long, ScheduledFuture<?>> runningTasks = new ConcurrentHashMap<>();
private final Map<Long, ReentrantLock> taskLocks = new ConcurrentHashMap<>();
private final Map<String, Consumer<String>> 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, "<empty>"))
);
@Override @Override
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) {
// 启动时加载所有运行中的任务
var tasks = taskMapper.selectList( var tasks = taskMapper.selectList(
new LambdaQueryWrapper<ScheduledTaskEntity>().eq(ScheduledTaskEntity::getStatus, 1)); new LambdaQueryWrapper<ScheduledTaskEntity>().eq(ScheduledTaskEntity::getStatus, 1));
for (ScheduledTaskEntity task : tasks) { for (ScheduledTaskEntity task : tasks) {
validateTaskTemplate(task);
addTask(task); addTask(task);
} }
log.info("动态定时任务加载完成, 共{}个", tasks.size()); log.info("动态定时任务加载完成, 共{}个", tasks.size());
} }
/** 添加并启动任务 */
public void addTask(ScheduledTaskEntity task) { public void addTask(ScheduledTaskEntity task) {
validateTaskTemplate(task);
withTaskLock(task.getId(), () -> {
if (isFutureActive(runningTasks.get(task.getId()))) {
log.warn("定时任务已在运行,跳过重复启动: taskId={}", task.getId());
return;
}
ScheduledFuture<?> future = taskScheduler.schedule( ScheduledFuture<?> future = taskScheduler.schedule(
() -> executeTask(task), new CronTrigger(task.getCronExpression())); () -> executeTask(task), new CronTrigger(task.getCronExpression()));
if (future == null) {
throw new BizException(ResultCode.INTERNAL_ERROR, "定时任务启动失败");
}
runningTasks.put(task.getId(), future); runningTasks.put(task.getId(), future);
log.info("定时任务已启动: {} [{}]", task.getTaskName(), task.getCronExpression()); log.info("定时任务已启动: {} [{}]", task.getTaskName(), task.getCronExpression());
});
} }
/** 停止任务 */
public void removeTask(Long taskId) { public void removeTask(Long taskId) {
ScheduledFuture<?> future = runningTasks.remove(taskId); withTaskLock(taskId, () -> doRemoveTask(taskId));
if (future != null) {
future.cancel(false);
log.info("定时任务已停止: taskId={}", taskId);
}
} }
/** 重启任务 */
public void restartTask(ScheduledTaskEntity task) { public void restartTask(ScheduledTaskEntity task) {
removeTask(task.getId()); validateTaskTemplate(task);
addTask(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) { private void executeTask(ScheduledTaskEntity task) {
try { try {
Object bean = applicationContext.getBean(task.getBeanName()); Consumer<String> taskExecutor = taskTemplates.get(templateKey(task.getBeanName(), task.getMethodName()));
Method method; if (taskExecutor == null) {
if (task.getMethodParams() != null && !task.getMethodParams().isEmpty()) { throw new BizException(ResultCode.FORBIDDEN, "任务模板未授权");
method = bean.getClass().getMethod(task.getMethodName(), String.class);
method.invoke(bean, task.getMethodParams());
} else {
method = bean.getClass().getMethod(task.getMethodName());
method.invoke(bean);
} }
taskExecutor.accept(task.getMethodParams());
} catch (Exception e) { } catch (Exception e) {
log.error("定时任务执行失败: {}", task.getTaskName(), e); log.error("定时任务执行失败: {}", task.getTaskName(), e);
} }
} }
/** 创建任务 */
public void create(ScheduledTaskEntity task) { public void create(ScheduledTaskEntity task) {
normalizeAndValidateTask(task);
task.setStatus(1); task.setStatus(1);
taskMapper.insert(task); taskMapper.insert(task);
addTask(task); addTask(task);
} }
/** 更新任务 */
public void update(ScheduledTaskEntity 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); taskMapper.updateById(task);
if (task.getStatus() != null && task.getStatus() == 1) { ScheduledTaskEntity latest = requireTask(task.getId());
restartTask(task); if (Integer.valueOf(1).equals(latest.getStatus())) {
restartTask(latest);
} else { } else {
removeTask(task.getId()); removeTask(task.getId());
} }
} }
/** 暂停任务 */
public void pause(Long taskId) { public void pause(Long taskId) {
withTaskLock(taskId, () -> {
ScheduledTaskEntity task = requireTask(taskId);
if (Integer.valueOf(0).equals(task.getStatus())) {
doRemoveTask(taskId);
return;
}
ScheduledTaskEntity entity = new ScheduledTaskEntity(); ScheduledTaskEntity entity = new ScheduledTaskEntity();
entity.setId(taskId); entity.setId(taskId);
entity.setStatus(0); entity.setStatus(0);
taskMapper.updateById(entity); taskMapper.updateById(entity);
removeTask(taskId); doRemoveTask(taskId);
});
} }
/** 恢复任务 */
public void resume(Long taskId) { public void resume(Long taskId) {
ScheduledTaskEntity task = taskMapper.selectById(taskId); withTaskLock(taskId, () -> {
if (task == null) { ScheduledTaskEntity task = requireTask(taskId);
throw new BizException(ResultCode.NOT_FOUND, "任务不存在"); 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); task.setStatus(1);
taskMapper.updateById(task); }
addTask(task); 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) { public void delete(Long taskId) {
requireTask(taskId);
removeTask(taskId); removeTask(taskId);
taskMapper.deleteById(taskId); taskMapper.deleteById(taskId);
taskLocks.remove(taskId);
} }
public PageResult<ScheduledTaskEntity> page(TaskQueryBo query) { public PageResult<ScheduledTaskEntity> page(TaskQueryBo query) {
@@ -140,10 +186,65 @@ public class DynamicTaskService implements ApplicationRunner {
} }
public ScheduledTaskEntity detail(Long id) { public ScheduledTaskEntity detail(Long id) {
ScheduledTaskEntity entity = taskMapper.selectById(id); return requireTask(id);
if (entity == null) { }
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, "任务不存在"); 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;
} }
} }

View File

@@ -21,6 +21,8 @@ import com.test.demo.service.UserService;
import com.test.demo.vo.LoginVo; import com.test.demo.vo.LoginVo;
import com.test.demo.vo.UserVo; import com.test.demo.vo.UserVo;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -34,7 +36,6 @@ import java.util.concurrent.TimeUnit;
public class AuthServiceImpl implements AuthService { public class AuthServiceImpl implements AuthService {
private static final long LOGIN_FAIL_EXPIRE_MINUTES = 15; private static final long LOGIN_FAIL_EXPIRE_MINUTES = 15;
private static final long LOGIN_FAIL_MAX_TIMES = 5; private static final long LOGIN_FAIL_MAX_TIMES = 5;
private final UserService userService; private final UserService userService;
@@ -94,11 +95,6 @@ public class AuthServiceImpl implements AuthService {
if (!captchaService.verify(bo.getCaptchaKey(), bo.getCaptchaCode())) { if (!captchaService.verify(bo.getCaptchaKey(), bo.getCaptchaCode())) {
throw new BizException(ResultCode.BAD_REQUEST, "验证码错误或已过期"); throw new BizException(ResultCode.BAD_REQUEST, "验证码错误或已过期");
} }
long count = userService.count(new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getUsername, bo.getUsername()));
if (count > 0) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
UserEntity user = new UserEntity(); UserEntity user = new UserEntity();
user.setUsername(bo.getUsername()); user.setUsername(bo.getUsername());
user.setNickname(bo.getNickname()); user.setNickname(bo.getNickname());
@@ -109,7 +105,11 @@ public class AuthServiceImpl implements AuthService {
user.setSalt(salt); user.setSalt(salt);
user.setPassword(SM3Util.hash(bo.getPassword(), salt)); user.setPassword(SM3Util.hash(bo.getPassword(), salt));
user.setStatus(UserStatusEnum.NORMAL); user.setStatus(UserStatusEnum.NORMAL);
try {
userService.save(user); userService.save(user);
} catch (DuplicateKeyException | DataIntegrityViolationException ex) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
} }
@Override @Override
@@ -139,14 +139,17 @@ public class AuthServiceImpl implements AuthService {
private long getLoginFailTimes(String key) { private long getLoginFailTimes(String key) {
Object value = redisTemplate.opsForValue().get(key); Object value = redisTemplate.opsForValue().get(key);
if (value == null) { 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) { private void increaseLoginFailTimes(String key) {
Long count = redisTemplate.opsForValue().increment(key); Long failTimes = redisTemplate.opsForValue().increment(key);
if (count != null && count == 1L) { if (failTimes != null && failTimes == 1L) {
redisTemplate.expire(key, LOGIN_FAIL_EXPIRE_MINUTES, TimeUnit.MINUTES); redisTemplate.expire(key, LOGIN_FAIL_EXPIRE_MINUTES, TimeUnit.MINUTES);
} }
} }

View File

@@ -14,12 +14,15 @@ import com.test.demo.entity.UserEntity;
import com.test.demo.enums.UserStatusEnum; import com.test.demo.enums.UserStatusEnum;
import com.test.demo.mapper.UserMapper; import com.test.demo.mapper.UserMapper;
import com.test.demo.security.crypto.SM3Util; import com.test.demo.security.crypto.SM3Util;
import com.test.demo.service.UserService;
import com.test.demo.util.ExcelUtil; import com.test.demo.util.ExcelUtil;
import com.test.demo.util.PasswordPolicyUtil;
import com.test.demo.vo.UserImportVo; import com.test.demo.vo.UserImportVo;
import com.test.demo.vo.UserVo; import com.test.demo.vo.UserVo;
import com.test.demo.service.UserService;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -62,20 +65,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
@Override @Override
public void add(UserBo bo) { public void add(UserBo bo) {
// 检查用户名唯一
long count = count(new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getUsername, bo.getUsername()));
if (count > 0) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
UserEntity entity = userConvert.toEntity(bo); UserEntity entity = userConvert.toEntity(bo);
// SM3加密密码
String salt = SM3Util.generateSalt(); String salt = SM3Util.generateSalt();
entity.setSalt(salt); entity.setSalt(salt);
entity.setPassword(SM3Util.hash(bo.getPassword(), salt)); entity.setPassword(SM3Util.hash(bo.getPassword(), salt));
entity.setStatus(UserStatusEnum.NORMAL); entity.setStatus(UserStatusEnum.NORMAL);
try {
save(entity); save(entity);
} catch (DuplicateKeyException | DataIntegrityViolationException ex) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
}
} }
@Override @Override
@@ -98,6 +97,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
@Override @Override
public void resetPassword(Long id, String newPassword) { public void resetPassword(Long id, String newPassword) {
PasswordPolicyUtil.validate(newPassword);
UserEntity entity = getById(id); UserEntity entity = getById(id);
if (entity == null) { if (entity == null) {
throw new BizException(ResultCode.NOT_FOUND, "用户不存在"); throw new BizException(ResultCode.NOT_FOUND, "用户不存在");
@@ -140,11 +140,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
if (StrUtil.isBlank(row.getUsername())) { if (StrUtil.isBlank(row.getUsername())) {
continue; continue;
} }
long count = count(new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getUsername, row.getUsername()));
if (count > 0) {
continue;
}
UserEntity entity = new UserEntity(); UserEntity entity = new UserEntity();
entity.setUsername(row.getUsername()); entity.setUsername(row.getUsername());
entity.setNickname(row.getNickname()); entity.setNickname(row.getNickname());
@@ -156,7 +151,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
entity.setSalt(salt); entity.setSalt(salt);
entity.setPassword(SM3Util.hash(rawPassword, salt)); entity.setPassword(SM3Util.hash(rawPassword, salt));
entity.setStatus(UserStatusEnum.NORMAL); entity.setStatus(UserStatusEnum.NORMAL);
try {
save(entity); save(entity);
} catch (DuplicateKeyException | DataIntegrityViolationException ex) {
throw new BizException(ResultCode.CONFLICT, "导入失败,用户名已存在: " + row.getUsername());
}
} }
} catch (IOException e) { } catch (IOException e) {
throw new BizException(ResultCode.INTERNAL_ERROR, "导入文件读取失败"); throw new BizException(ResultCode.INTERNAL_ERROR, "导入文件读取失败");

View File

@@ -2,6 +2,12 @@ package com.test.demo.util;
import cn.hutool.core.util.StrUtil; 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 是否安全 * @return 是否安全
*/ */
public static boolean isSafePath(String basePath, String filePath) { public static boolean isSafePath(String basePath, String filePath) {
if (StrUtil.isBlank(filePath)) { if (StrUtil.isBlank(basePath) || StrUtil.isBlank(filePath)) {
return false; return false;
} }
// 禁止路径穿越 try {
if (filePath.contains("..") || filePath.contains("./")) { 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; return false;
} }
// 禁止绝对路径
if (filePath.startsWith("/") || filePath.contains(":")) {
return false;
} }
// 禁止特殊字符
if (filePath.matches(".*[\\\\<>|\"?*].*")) { private static Path toRealPathIfPossible(Path path) throws IOException {
return false; 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();
} }
return true;
} }
/** /**

View File

@@ -84,3 +84,8 @@ management:
show-details: when-authorized show-details: when-authorized
mail: mail:
enabled: false enabled: false
app:
cors:
allowed-origins: ${APP_CORS_ALLOWED_ORIGINS:}