非法值校验

This commit is contained in:
cwchen 2025-09-29 17:53:19 +08:00
parent 3d2266c427
commit 2cf9297e9e
5 changed files with 354 additions and 32 deletions

View File

@ -16,7 +16,6 @@ import org.springframework.context.annotation.Configuration;
import com.bonus.common.filter.RepeatableFilter;
import com.bonus.common.filter.XssFilter;
import com.bonus.common.utils.StringUtils;
import org.springframework.core.Ordered;
/**
* Filter配置
@ -91,16 +90,6 @@ public class FilterConfig
return registration;
}
/*@Bean
public FilterRegistrationBean<ReplayAttackFilter> replayAttackFilter() {
FilterRegistrationBean<ReplayAttackFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ReplayAttackFilter());
registration.addUrlPatterns("/*");
registration.setOrder(1);
registration.setName("replayAttackFilter");
return registration;
}*/
@Bean
public FilterRegistrationBean<IpWhitelistFilter> ipWhitelistFilterRegistration(IpWhitelistFilter filter) {
FilterRegistrationBean<IpWhitelistFilter> registration = new FilterRegistrationBean<>();

View File

@ -3,6 +3,7 @@ package com.bonus.framework.config;
import java.util.concurrent.TimeUnit;
import com.bonus.framework.interceptor.ParamSecureHandler;
import com.bonus.framework.interceptor.ReplayAttackInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -31,6 +32,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired
private ParamSecureHandler paramSecureHandler;
@Autowired
private ReplayAttackInterceptor replayAttackInterceptor;
/** 不需要拦截地址 "/logout",*/
public static final String[] EXCLUDEURLS = { "/login", "/refresh" };
@ -42,9 +46,9 @@ public class ResourcesConfig implements WebMvcConfigurer
.addResourceLocations("file:" + BonusConfig.getProfile() + "/");
/** swagger配置 */
/*registry.addResourceHandler("/swagger-ui/**")
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
.setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());*/
.setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());
}
/**
@ -54,11 +58,23 @@ public class ResourcesConfig implements WebMvcConfigurer
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
//自定义拦截器
// 参数校验拦截器
registry.addInterceptor(paramSecureHandler)
.addPathPatterns("/**")
.excludePathPatterns(EXCLUDEURLS)
.order(-10);
// 防重放拦截器
registry.addInterceptor(replayAttackInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/smartArchives/captchaImage")
.excludePathPatterns("/smartArchives/login")
.excludePathPatterns("/smartArchives/logout")
.excludePathPatterns("/smartArchives/getInfo")
.excludePathPatterns("/smartArchives/getRouters")
.excludePathPatterns("/smartArchives/session/check")
.excludePathPatterns("/smartArchives/sys/config/getConfig")
.excludePathPatterns(EXCLUDEURLS)
.order(-15);
}
/**

View File

@ -1,3 +1,4 @@
/*
package com.bonus.framework.filter;
import javax.annotation.Resource;
@ -30,13 +31,15 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
*/
/**
* @className:ReplayAttackFilter
* @author:cwchen
* @date:2025-09-04-11:19
* @version:1.0
* @description:防重放攻击过滤器
*/
*//*
//@Configuration
@Slf4j
@Component
@ -215,17 +218,21 @@ public class ReplayAttackFilter implements Filter {
}
}
/**
*/
/**
* 生成请求的唯一签名标识用于Redis键
*/
*//*
private String generateRequestSignature(String userId, long timestamp, String requestUrl, String method) {
String rawString = userId + ":" + timestamp + ":" + method + ":" + requestUrl;
return sha256Hash(rawString);
}
/**
*/
/**
* 验证请求签名
*/
*//*
private boolean validateSignature(HttpServletRequest request, String userId,String secret,
long timestamp, String requestUrl, String receivedSignature) {
String encryptUserId = Sm4Utils.encrypt(userId);
@ -239,9 +246,11 @@ public class ReplayAttackFilter implements Filter {
return calculatedSignature.equals(receivedSignature);
}
/**
*/
/**
* 构建待签名字符串
*/
*//*
private String buildSignString(String userId, long timestamp, String requestUrl, String method) {
StringBuilder sb = new StringBuilder();
sb.append(userId)
@ -253,9 +262,11 @@ public class ReplayAttackFilter implements Filter {
}
/**
*/
/**
* 计算HMAC-SHA256签名
*/
*//*
private String calculateHMAC(String data, String key) {
try {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
@ -269,9 +280,11 @@ public class ReplayAttackFilter implements Filter {
}
}
/**
*/
/**
* SHA256哈希
*/
*//*
private String sha256Hash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
@ -310,4 +323,4 @@ public class ReplayAttackFilter implements Filter {
public void destroy() {
System.out.println("ReplayAttackFilter destroyed");
}
}
}*/

View File

@ -0,0 +1,304 @@
package com.bonus.framework.interceptor;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.bonus.common.constant.CacheConstants;
import com.bonus.common.core.domain.entity.SysUser;
import com.bonus.common.core.domain.model.LoginUser;
import com.bonus.common.core.redis.RedisCache;
import com.bonus.common.utils.encryption.Sm4Utils;
import com.bonus.framework.web.service.TokenService;
import com.bonus.system.domain.vo.SystemConfigVo;
import com.bonus.system.service.ISystemConfigService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @className: ReplayAttackInterceptor
* @author: cwchen
* @date: 2025-09-04-11:19
* @version: 1.0
* @description: 防重放攻击拦截器
*/
@Slf4j
@Component
public class ReplayAttackInterceptor implements HandlerInterceptor {
static List<String> ignoreUrlPatterns = new ArrayList<>();
static {
ignoreUrlPatterns.add("/smartArchives/captchaImage");
ignoreUrlPatterns.add("/smartArchives/login");
ignoreUrlPatterns.add("/smartArchives/logout");
ignoreUrlPatterns.add("/smartArchives/getInfo");
ignoreUrlPatterns.add("/smartArchives/getRouters");
ignoreUrlPatterns.add("/smartArchives/session/check");
ignoreUrlPatterns.add("/smartArchives/sys/config/getConfig");
}
private final RedisCache redisUtil;
@Autowired
private TokenService tokenService;
@Autowired
private ISystemConfigService configService;
// 时间戳允许的误差范围毫秒
private static final long TIMESTAMP_TOLERANCE = 5 * 60 * 1000; // 5分钟
// 请求签名在Redis中的过期时间
private static final int SIGNATURE_EXPIRE_SECONDS = (int) (TIMESTAMP_TOLERANCE * 2 / 1000);
// Redis键前缀
private static final String SIGNATURE_KEY_PREFIX = "replay:signature:";
private static final long TIMESTAMP_TOLERANCE2 = 15 * 60 * 1000; // 15分钟
// 请求签名在Redis中的过期时间
private static final int SIGNATURE_EXPIRE_SECONDS2 = (int) (TIMESTAMP_TOLERANCE2 * 2 / 1000);
public ReplayAttackInterceptor(RedisCache redisUtil) {
this.redisUtil = redisUtil;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 跳过OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String requestURI = request.getRequestURI();
boolean shouldIgnore = ignoreUrlPatterns.contains(requestURI);
if (shouldIgnore) {
// 忽略该请求的处理
return true;
}
// 检查系统配置
if (isReplayAttackDisabled()) {
return true;
}
// 获取当前登录人
LoginUser loginUser = tokenService.getLoginUser(request);
Long userId = Optional.ofNullable(loginUser)
.map(LoginUser::getUserId)
.orElse(null);
// 密钥
String secret = Optional.ofNullable(loginUser)
.map(LoginUser::getUser)
.map(SysUser::getSecret)
.orElse(null);
try {
// 1. 获取必要请求头
String timestampStr = request.getHeader("timestamp");
String signature = request.getHeader("X-Signature");
// 2. 检查必要参数是否存在
if (timestampStr == null || secret == null || userId == null || signature == null) {
sendErrorResponse(response, "Missing required headers", HttpServletResponse.SC_BAD_REQUEST);
return false;
}
// 3. 验证时间戳格式和有效性
long timestamp;
try {
timestamp = Long.parseLong(timestampStr);
} catch (NumberFormatException e) {
sendErrorResponse(response, "Invalid timestamp format", HttpServletResponse.SC_BAD_REQUEST);
return false;
}
// 4. 检查时间戳是否在允许范围内
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - timestamp) > TIMESTAMP_TOLERANCE) {
sendErrorResponse(response, "Timestamp expired or invalid", HttpServletResponse.SC_BAD_REQUEST);
return false;
}
// 5. 获取请求URL
String requestUrl = request.getRequestURI();
String queryString = request.getQueryString();
if (queryString != null) {
requestUrl += "?" + queryString;
}
// 6. 生成请求的唯一签名标识
String requestSignature = generateRequestSignature(String.valueOf(userId), timestamp, requestUrl, request.getMethod());
// 7. 检查签名是否已使用防重放
String signatureKey = SIGNATURE_KEY_PREFIX + requestSignature;
if (redisUtil.hasKey(signatureKey)) {
sendErrorResponse(response, "Request replay detected", HttpServletResponse.SC_BAD_REQUEST);
return false;
}
// 8. 验证请求签名防止篡改
if (!validateSignature(request, String.valueOf(userId), secret, timestamp, requestUrl, signature)) {
sendErrorResponse(response, "Invalid signature", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return false;
}
// 9. 将签名存储到Redis设置过期时间
Boolean stored = redisUtil.setNxCacheObject(signatureKey,
String.valueOf(currentTime),
(long) SIGNATURE_EXPIRE_SECONDS,
TimeUnit.SECONDS);
if (stored == null || !stored) {
sendErrorResponse(response, "Failed to store signature", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return false;
}
// 验证通过继续处理请求
return true;
} catch (Exception e) {
sendErrorResponse(response, "Server error: " + e.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return false;
}
}
/**
* 检查重放攻击防护是否被禁用
*/
private boolean isReplayAttackDisabled() {
Object cacheObject = redisUtil.getCacheObject(CacheConstants.SYSTEM_CONFIG_VOS);
if (Objects.isNull(cacheObject)) {
List<SystemConfigVo> systemConfigVos = configService.listConfig();
Boolean stored = redisUtil.setNxCacheObject(CacheConstants.SYSTEM_CONFIG_VOS,
JSON.toJSONString(systemConfigVos),
(long) SIGNATURE_EXPIRE_SECONDS,
TimeUnit.SECONDS);
if (CollectionUtils.isNotEmpty(systemConfigVos)) {
SystemConfigVo config = systemConfigVos.stream()
.filter(item -> CacheConstants.REPLAY_ATTACK.equals(item.getConfigCode()))
.findFirst()
.orElse(null);
if (Objects.nonNull(config)) {
String useStatus = config.getUseStatus();
return Objects.equals("1", useStatus);
}
}
} else {
String cacheStr = (String) cacheObject;
List<SystemConfigVo> systemConfigVos = JSON.parseArray(cacheStr, SystemConfigVo.class);
if (CollectionUtils.isNotEmpty(systemConfigVos)) {
SystemConfigVo config = systemConfigVos.stream()
.filter(item -> CacheConstants.REPLAY_ATTACK.equals(item.getConfigCode()))
.findFirst()
.orElse(null);
if (Objects.nonNull(config)) {
String useStatus = config.getUseStatus();
return Objects.equals("1", useStatus);
}
}
}
return false;
}
/**
* 生成请求的唯一签名标识用于Redis键
*/
private String generateRequestSignature(String userId, long timestamp, String requestUrl, String method) {
String rawString = userId + ":" + timestamp + ":" + method + ":" + requestUrl;
return sha256Hash(rawString);
}
/**
* 验证请求签名
*/
private boolean validateSignature(HttpServletRequest request, String userId, String secret,
long timestamp, String requestUrl, String receivedSignature) {
String encryptUserId = Sm4Utils.encrypt(userId);
String encryptSecret = Sm4Utils.encrypt(secret);
log.debug("Request URL: {}", requestUrl);
// 构建待签名字符串
String signString = buildSignString(encryptUserId, timestamp, requestUrl, request.getMethod());
// 使用HMAC-SHA256计算签名
String calculatedSignature = calculateHMAC(signString, encryptSecret);
return calculatedSignature.equals(receivedSignature);
}
/**
* 构建待签名字符串
*/
private String buildSignString(String userId, long timestamp, String requestUrl, String method) {
StringBuilder sb = new StringBuilder();
sb.append(userId)
.append(timestamp)
.append(method.toUpperCase())
.append(requestUrl);
return sb.toString();
}
/**
* 计算HMAC-SHA256签名
*/
private String calculateHMAC(String data, String key) {
try {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
javax.crypto.spec.SecretKeySpec secretKey =
new javax.crypto.spec.SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate HMAC", e);
}
}
/**
* SHA256哈希
*/
private String sha256Hash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate SHA256", e);
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private void sendErrorResponse(HttpServletResponse response, String message, int status)
throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(String.format(
"{\"error\": \"%s\", \"code\": %d, \"timestamp\": %d}",
message, status, System.currentTimeMillis()
));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 可选的清理操作
}
}

12
pom.xml
View File

@ -35,7 +35,7 @@
<logback.version>1.2.13</logback.version>
<spring-security.version>5.7.12</spring-security.version>
<spring-framework.version>5.3.39</spring-framework.version>
<profiles.active>test</profiles.active> <!-- 默认值 -->
<profiles.active>dev</profiles.active> <!-- 默认值 -->
</properties>
<!-- 依赖声明 -->
@ -302,9 +302,9 @@
<properties>
<profiles.active>dev</profiles.active>
</properties>
<!--<activation>
<activation>
<activeByDefault>true</activeByDefault>
</activation>-->
</activation>
</profile>
<!-- 测试环境 -->
@ -313,9 +313,9 @@
<properties>
<profiles.active>test</profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<!-- <activation>-->
<!-- <activeByDefault>true</activeByDefault>-->
<!-- </activation>-->
</profile>
<!-- 生产环境 -->