非法值校验
This commit is contained in:
parent
3d2266c427
commit
2cf9297e9e
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
|
|
|||
|
|
@ -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
12
pom.xml
|
|
@ -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>
|
||||
|
||||
<!-- 生产环境 -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue