diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/ReplayAttackInterceptor.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/ReplayAttackInterceptor.java index 2f0967b..a246386 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/ReplayAttackInterceptor.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/ReplayAttackInterceptor.java @@ -23,10 +23,12 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; @Slf4j public class ReplayAttackInterceptor implements HandlerInterceptor { - // 请求时间戳有效期(毫秒):5分钟 - private static final long TIMESTAMP_VALIDITY = 5 * 60 * 1000; - // nonce 有效期(毫秒):10分钟 - private static final long NONCE_EXPIRE_TIME = 10 * 60 * 1000; + // 请求时间戳有效期(毫秒):10分钟,兼容移动端网络抖动与时钟偏差 + private static final long TIMESTAMP_VALIDITY = 10 * 60 * 1000; + // 允许少量未来时间偏移(毫秒):2分钟,兼容秒级时间戳和终端时钟轻微超前 + private static final long ALLOWED_FUTURE_DRIFT = 2 * 60 * 1000; + // nonce 有效期(毫秒):15分钟 + private static final long NONCE_EXPIRE_TIME = 15 * 60 * 1000; // 最大存储 nonce 数(防止内存溢出) private static final int MAX_NONCES = 100000; @@ -91,24 +93,35 @@ public class ReplayAttackInterceptor implements HandlerInterceptor { String timestampStr = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); + String userAgent = request.getHeader("User-Agent"); + // 检查参数是否存在 if (timestampStr == null || timestampStr.isEmpty() || nonce == null || nonce.isEmpty()) { + String missingHeader = (timestampStr == null || timestampStr.isEmpty()) && (nonce == null || nonce.isEmpty()) + ? "X-Timestamp,X-Nonce" + : ((timestampStr == null || timestampStr.isEmpty()) ? "X-Timestamp" : "X-Nonce"); + log.warn("防重放校验失败: uri={}, method={}, ua={}, missingHeaders={}", + requestUri, request.getMethod(), userAgent, missingHeader); writeErrorResponse(response, "缺少安全验证参数"); return false; } - // 验证时间戳 + // 验证时间戳(兼容 10 位秒级与 13 位毫秒级) long timestamp; try { - timestamp = Long.parseLong(timestampStr); + timestamp = normalizeTimestamp(timestampStr); } catch (NumberFormatException e) { + log.warn("防重放校验失败: uri={}, method={}, ua={}, invalidTimestamp={}, error={}", + requestUri, request.getMethod(), userAgent, timestampStr, e.getMessage()); writeErrorResponse(response, "无效的时间戳参数"); return false; } long now = System.currentTimeMillis(); - // 修复:不允许未来时间戳,防止预发送攻击 - if (timestamp > now || now - timestamp > TIMESTAMP_VALIDITY) { + long timeDiff = now - timestamp; + if (timeDiff < -ALLOWED_FUTURE_DRIFT || timeDiff > TIMESTAMP_VALIDITY) { + log.warn("防重放校验失败: uri={}, method={}, ua={}, timestamp={}, normalizedTimestamp={}, now={}, diffMs={}, validityMs={}", + requestUri, request.getMethod(), userAgent, timestampStr, timestamp, now, timeDiff, TIMESTAMP_VALIDITY); writeErrorResponse(response, "请求时间戳无效或已过期"); return false; } @@ -117,6 +130,8 @@ public class ReplayAttackInterceptor implements HandlerInterceptor { lock.readLock().lock(); try { if (usedNonces.containsKey(nonce)) { + log.warn("防重放校验失败: uri={}, method={}, ua={}, nonce={}, reason=reused_nonce", + requestUri, request.getMethod(), userAgent, nonce); writeErrorResponse(response, "检测到重放攻击,请求已被拒绝"); return false; } @@ -135,6 +150,15 @@ public class ReplayAttackInterceptor implements HandlerInterceptor { return true; } + + private long normalizeTimestamp(String timestampStr) { + long parsedTimestamp = Long.parseLong(timestampStr); + if (timestampStr.length() == 10) { + return parsedTimestamp * 1000; + } + return parsedTimestamp; + } + /** * 清理过期的 nonce */