SpringBoot實現(xiàn)防重放攻擊的五種方案
一、重放攻擊基本概念
1.1 什么是重放攻擊
重放攻擊是一種網(wǎng)絡攻擊手段,攻擊者截獲一個有效的數(shù)據(jù)傳輸,然后在稍后的時間重新發(fā)送相同的數(shù)據(jù),以實現(xiàn)欺騙系統(tǒng)的目的。在Web應用中,這通常表現(xiàn)為重復提交相同的請求,比如:
- 重復提交訂單付款請求
- 重復使用過期的訪問令牌
- 重復提交表單數(shù)據(jù)
- 重新發(fā)送包含認證信息的請求
1.2 重放攻擊的危害
重放攻擊可能導致以下安全問題:
- 資金損失:重復執(zhí)行支付交易
- 資源耗盡:大量重復請求導致系統(tǒng)資源枯竭
- 數(shù)據(jù)不一致:重復提交導致數(shù)據(jù)重復或狀態(tài)混亂
- 業(yè)務邏輯被繞過:繞過設計中的業(yè)務規(guī)則
- 權限提升:復用他人有效的認證信息
二、時間戳+請求超時機制
2.1 基本原理
這種方案要求客戶端在每個請求中附帶當前時間戳,服務器收到請求后,檢查時間戳是否在允許的時間窗口內(nèi)(通常為幾分鐘)。
如果請求的時間戳超出時間窗口,則認為是過期請求或潛在的重放攻擊,拒絕處理該請求。
2.2 SpringBoot實現(xiàn)
首先,創(chuàng)建一個請求包裝類,包含時間戳字段:
@Data public class ApiRequest<T> { private Long timestamp; // 請求時間戳,毫秒級 private T data; // 實際請求數(shù)據(jù) }
然后,創(chuàng)建一個攔截器來檢查請求時間戳:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class RequestBodyCachingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (isRequestBodyEligible(request)) { ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); filterChain.doFilter(wrappedRequest, response); } else { filterChain.doFilter(request, response); } } private boolean isRequestBodyEligible(HttpServletRequest request) { String method = request.getMethod(); return "POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method) || "DELETE".equals(method); } } @Component @Slf4j public class TimestampInterceptor implements HandlerInterceptor { private static final long ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分鐘時間窗口 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } // 檢查是否需要進行時間戳驗證 HandlerMethod handlerMethod = (HandlerMethod) handler; if (!handlerMethod.getMethod().isAnnotationPresent(CheckTimestamp.class)) { return true; } // 獲取請求體 String requestBody = getRequestBody(request); try { // 解析請求體,獲取時間戳 ApiRequest<?> apiRequest = new ObjectMapper().readValue(requestBody, ApiRequest.class); Long timestamp = apiRequest.getTimestamp(); if (timestamp == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Missing timestamp"); return false; } // 檢查時間戳是否在允許的時間窗口內(nèi) long currentTime = System.currentTimeMillis(); if (currentTime - timestamp > ALLOWED_TIME_WINDOW || timestamp > currentTime) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Request expired or invalid timestamp"); return false; } return true; } catch (Exception e) { log.error("Error processing timestamp", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Invalid request format"); return false; } } private String getRequestBody(HttpServletRequest request) throws IOException { // 針對ContentCachingRequestWrapper的處理 if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request; // 讀取緩存的內(nèi)容 byte[] buf = wrapper.getContentAsByteArray(); if (buf.length > 0) { return new String(buf, wrapper.getCharacterEncoding()); } } // 針對MultiReadHttpServletRequest的處理 try (BufferedReader reader = request.getReader()) { return reader.lines().collect(Collectors.joining(System.lineSeparator())); } } }
創(chuàng)建一個注解,用于標記需要進行時間戳驗證的接口:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckTimestamp { }
配置攔截器:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TimestampInterceptor timestampInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timestampInterceptor) .addPathPatterns("/api/**"); } }
在需要防止重放攻擊的接口上添加注解:
@RestController @RequestMapping("/api/orders") public class OrderController { @PostMapping @CheckTimestamp public ResponseEntity<?> createOrder(@RequestBody ApiRequest<OrderCreateRequest> request) { // 處理訂單創(chuàng)建邏輯 return ResponseEntity.ok().build(); } }
2.3 優(yōu)缺點分析
優(yōu)點:
- 實現(xiàn)簡單,無需額外存儲
- 不依賴會話狀態(tài),適合分布式系統(tǒng)
- 客戶端實現(xiàn)簡單,只需添加時間戳
缺點:
- 需要客戶端和服務器時間同步
- 時間窗口存在權衡:太短影響用戶體驗,太長降低安全性
- 無法防止時間窗口內(nèi)的重放攻擊
- 不適合時間敏感的高安全場景
三、Nonce隨機數(shù)+Redis緩存
3.1 基本原理
Nonce(Number used once)是一個只使用一次的隨機數(shù)。在此方案中,客戶端每次請求都生成一個唯一的隨機數(shù),并發(fā)送給服務器。
服務器將使用過的Nonce存儲在Redis緩存中一段時間,拒絕任何使用重復Nonce的請求。這種方式可以有效防止重放攻擊,因為每個有效請求都需要一個從未使用過的Nonce。
3.2 SpringBoot實現(xiàn)
首先,擴展請求包裝類,添加Nonce字段:
@Data public class ApiRequest<T> { private Long timestamp; // 請求時間戳,毫秒級 private String nonce; // 隨機數(shù),每次請求唯一 private T data; // 實際請求數(shù)據(jù) }
添加Redis配置:
@Configuration @EnableCaching public class RedisConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); return template; } }
創(chuàng)建Nonce檢查攔截器:
@Component @Slf4j public class NonceInterceptor implements HandlerInterceptor { private static final long NONCE_EXPIRE_SECONDS = 3600; // Nonce有效期1小時 @Autowired private RedisTemplate<String, String> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; if (!handlerMethod.getMethod().isAnnotationPresent(CheckNonce.class)) { return true; } String requestBody = getRequestBody(request); try { ApiRequest<?> apiRequest = new ObjectMapper().readValue(requestBody, ApiRequest.class); String nonce = apiRequest.getNonce(); if (StringUtils.isEmpty(nonce)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Missing nonce"); return false; } // 檢查時間戳 Long timestamp = apiRequest.getTimestamp(); if (timestamp == null || System.currentTimeMillis() - timestamp > 5 * 60 * 1000) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Request expired or invalid timestamp"); return false; } // 檢查Nonce是否已使用 String nonceKey = "nonce:" + nonce; Boolean isFirstUse = redisTemplate.opsForValue().setIfAbsent(nonceKey, timestamp.toString(), NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS); if (isFirstUse == null || !isFirstUse) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Duplicate request or replay attack detected"); log.warn("Duplicate nonce detected: {}", nonce); return false; } return true; } catch (Exception e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Invalid request format"); return false; } } // getRequestBody方法同上 }
創(chuàng)建注解,標記需要進行Nonce驗證的接口:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckNonce { }
在需要防止重放攻擊的接口上添加注解:
@RestController @RequestMapping("/api/payments") public class PaymentController { @PostMapping @CheckNonce public ResponseEntity<?> processPayment(@RequestBody ApiRequest<PaymentRequest> request) { // 處理支付邏輯 return ResponseEntity.ok().build(); } }
3.3 客戶端生成Nonce示例
public class ApiClient { public static ApiRequest<T> createRequest(T data) { ApiRequest<T> request = new ApiRequest<>(); request.setTimestamp(System.currentTimeMillis()); request.setNonce(UUID.randomUUID().toString()); request.setData(data); return request; } }
3.4 優(yōu)缺點分析
優(yōu)點:
- 安全性高,每個請求都必須使用唯一的Nonce
- 可以有效防止在任何時間窗口內(nèi)的重放攻擊
- 結合時間戳可以雙重保障
缺點:
- 需要存儲使用過的Nonce,增加系統(tǒng)復雜性
- 在分布式系統(tǒng)中需要共享Nonce存儲
- 對Redis等存儲系統(tǒng)有依賴
- 客戶端需要生成唯一Nonce,實現(xiàn)相對復雜
四、冪等性令牌機制
4.1 基本原理
冪等性令牌機制是一種專門針對非冪等操作(如創(chuàng)建訂單、支付等)設計的防重放方案。
服務器先生成一個一次性的令牌并提供給客戶端,客戶端在執(zhí)行操作時必須提交這個令牌,服務器驗證令牌有效后執(zhí)行操作并立即使令牌失效,從而保證操作不會重復執(zhí)行。
4.2 SpringBoot實現(xiàn)
首先,創(chuàng)建令牌服務:
@Service @Slf4j public class IdempotencyTokenService { private static final String TOKEN_PREFIX = "idempotency_token:"; private static final long TOKEN_EXPIRE_MINUTES = 30; // 令牌有效期30分鐘 @Autowired private RedisTemplate<String, String> redisTemplate; /** * 生成冪等性令牌 */ public String generateToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set( TOKEN_PREFIX + token, "UNUSED", TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES ); return token; } /** * 驗證并消費令牌 * @return true表示令牌有效且已成功消費,false表示令牌無效或已被消費 */ public boolean validateAndConsumeToken(String token) { if (StringUtils.isEmpty(token)) { return false; } String key = TOKEN_PREFIX + token; // 使用Redis的原子操作驗證并更新令牌狀態(tài) String script = "if redis.call('get', KEYS[1]) == 'UNUSED' then " + "redis.call('set', KEYS[1], 'USED') " + "return 1 " + "else " + "return 0 " + "end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key) ); return result != null && result == 1; } }
創(chuàng)建獲取令牌的API:
@RestController @RequestMapping("/api/tokens") public class TokenController { @Autowired private IdempotencyTokenService tokenService; @PostMapping public ResponseEntity<Map<String, String>> generateToken() { String token = tokenService.generateToken(); Map<String, String> response = Collections.singletonMap("token", token); return ResponseEntity.ok(response); } }
創(chuàng)建冪等性檢查攔截器:
@Component @Slf4j public class IdempotencyInterceptor implements HandlerInterceptor { @Autowired private IdempotencyTokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; IdempotentOperation annotation = handlerMethod.getMethod().getAnnotation(IdempotentOperation.class); if (annotation == null) { return true; } // 從請求頭獲取冪等性令牌 String token = request.getHeader("Idempotency-Token"); if (StringUtils.isEmpty(token)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Missing idempotency token"); return false; } // 驗證并消費令牌 boolean isValid = tokenService.validateAndConsumeToken(token); if (!isValid) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Invalid or already used idempotency token"); log.warn("Attempt to reuse idempotency token: {}", token); return false; } return true; } }
創(chuàng)建冪等性操作注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface IdempotentOperation { }
在需要保證冪等性的API上使用注解:
@RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; @PostMapping @IdempotentOperation public ResponseEntity<?> createOrder(@RequestBody OrderCreateRequest request) { // 創(chuàng)建訂單 OrderDTO order = orderService.createOrder(request); return ResponseEntity.ok(order); } }
4.3 客戶端使用示例
// 第一步: 獲取冪等性令牌 const tokenResponse = await fetch('/api/tokens', { method: 'POST' }); const { token } = await tokenResponse.json(); // 第二步: 使用令牌提交請求 const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Token': token }, body: JSON.stringify({ // 訂單數(shù)據(jù) }) });
4.4 優(yōu)缺點分析
優(yōu)點:
- 專為非冪等操作設計,安全性高
- 客戶端必須先獲取令牌,可以有效防止未授權請求
- 服務端控制令牌生成和驗證,安全可控
- 可以與業(yè)務邏輯完美結合
缺點:
- 需要額外的獲取令牌請求,增加交互復雜性
- 依賴外部存儲系統(tǒng)保存令牌狀態(tài)
- 對客戶端有特定要求,實現(xiàn)相對復雜
- 令牌有效期管理需要權衡
五、請求簽名認證
5.1 基本原理
請求簽名認證方案通過對請求參數(shù)、時間戳、隨機數(shù)等信息進行加密簽名,確保請求在傳輸過程中不被篡改,同時結合時間戳和隨機數(shù)防止重放攻擊。
該方案通常用于API安全性要求較高的場景,如支付、金融等領域。
5.2 SpringBoot實現(xiàn)
首先,創(chuàng)建請求簽名工具類:
@Component public class SignatureUtils { /** * 生成簽名 * @param params 參與簽名的參數(shù) * @param timestamp 時間戳 * @param nonce 隨機數(shù) * @param secretKey 密鑰 * @return 簽名 */ public String generateSignature(Map<String, String> params, long timestamp, String nonce, String secretKey) { // 1. 按參數(shù)名ASCII碼排序 Map<String, String> sortedParams = new TreeMap<>(params); // 2. 構建簽名字符串 StringBuilder builder = new StringBuilder(); for (Map.Entry<String, String> entry : sortedParams.entrySet()) { if (StringUtils.isNotEmpty(entry.getValue())) { builder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } // 3. 添加時間戳和隨機數(shù) builder.append("timestamp=").append(timestamp).append("&"); builder.append("nonce=").append(nonce).append("&"); builder.append("key=").append(secretKey); // 4. 進行MD5簽名 return DigestUtils.md5DigestAsHex(builder.toString().getBytes(StandardCharsets.UTF_8)); } /** * 驗證簽名 */ public boolean verifySignature(Map<String, String> params, long timestamp, String nonce, String signature, String secretKey) { String calculatedSignature = generateSignature(params, timestamp, nonce, secretKey); return calculatedSignature.equals(signature); } }
創(chuàng)建簽名驗證攔截器:
@Component @Slf4j public class SignatureInterceptor implements HandlerInterceptor { private static final long ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分鐘時間窗口 @Autowired private SignatureUtils signatureUtils; @Autowired private RedisTemplate<String, String> redisTemplate; @Value("${api.secret-key}") private String secretKey; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; if (!handlerMethod.getMethod().isAnnotationPresent(CheckSignature.class)) { return true; } try { // 1. 獲取請求頭中的簽名信息 String signature = request.getHeader("X-Signature"); String timestampStr = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); String appId = request.getHeader("X-App-Id"); if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestampStr) || StringUtils.isEmpty(nonce) || StringUtils.isEmpty(appId)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Missing signature parameters"); return false; } // 2. 檢查時間戳是否在允許的時間窗口內(nèi) long timestamp = Long.parseLong(timestampStr); long currentTime = System.currentTimeMillis(); if (currentTime - timestamp > ALLOWED_TIME_WINDOW || timestamp > currentTime) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Request expired or invalid timestamp"); return false; } // 3. 檢查nonce是否已使用 String nonceKey = "signature_nonce:" + nonce; Boolean isFirstUse = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", ALLOWED_TIME_WINDOW, TimeUnit.MILLISECONDS); if (isFirstUse == null || !isFirstUse) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Duplicate request or replay attack detected"); log.warn("Duplicate nonce detected: {}", nonce); return false; } // 4. 獲取請求參數(shù) Map<String, String> params = new HashMap<>(); // 從請求體或URL參數(shù)中獲取參數(shù)... if (request.getMethod().equals("GET")) { request.getParameterMap().forEach((key, values) -> { if (values != null && values.length > 0) { params.put(key, values[0]); } }); } else { // 解析請求體,這里簡化處理 String requestBody = getRequestBody(request); if (StringUtils.isNotEmpty(requestBody)) { try { Map<String, Object> bodyMap = new ObjectMapper().readValue(requestBody, Map.class); bodyMap.forEach((key, value) -> { if (value != null) { params.put(key, value.toString()); } }); } catch (Exception e) { log.error("Failed to parse request body", e); } } } // 5. 驗證簽名 boolean isValid = signatureUtils.verifySignature(params, timestamp, nonce, signature, secretKey); if (!isValid) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Invalid signature"); log.warn("Invalid signature detected for appId: {}", appId); return false; } return true; } catch (Exception e) { log.error("Signature verification error", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Signature verification failed"); return false; } } // getRequestBody方法同上 }
創(chuàng)建簽名驗證注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckSignature { }
在需要簽名驗證的API上使用注解:
@RestController @RequestMapping("/api/secure") public class SecureApiController { @GetMapping("/data") @CheckSignature public ResponseEntity<?> getSecureData() { // 處理安全數(shù)據(jù) return ResponseEntity.ok(Map.of("data", "Secure data")); } @PostMapping("/transaction") @CheckSignature public ResponseEntity<?> processTransaction(@RequestBody TransactionRequest request) { // 處理交易 return ResponseEntity.ok(Map.of("result", "success")); } }
5.3 客戶端簽名示例
public class ApiClient { private static final String APP_ID = "your-app-id"; private static final String SECRET_KEY = "your-secret-key"; public static <T> String callSecureApi(String url, T requestBody, HttpMethod method) throws Exception { // 1. 準備簽名參數(shù) long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString(); // 2. 請求參數(shù)轉(zhuǎn)換為Map Map<String, String> params = new HashMap<>(); if (requestBody != null) { ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(requestBody); Map<String, Object> map = mapper.readValue(json, Map.class); map.forEach((key, value) -> { if (value != null) { params.put(key, value.toString()); } }); } // 3. 生成簽名 String signature = generateSignature(params, timestamp, nonce, SECRET_KEY); // 4. 發(fā)起請求 HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(method.name()); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("X-App-Id", APP_ID); connection.setRequestProperty("X-Timestamp", String.valueOf(timestamp)); connection.setRequestProperty("X-Nonce", nonce); connection.setRequestProperty("X-Signature", signature); // 設置請求體... // 獲取響應... return "Response"; } private static String generateSignature(Map<String, String> params, long timestamp, String nonce, String secretKey) { // 實現(xiàn)與服務端相同的簽名算法 // ... } }
5.4 優(yōu)缺點分析
優(yōu)點:
- 安全性高,可以同時防止重放攻擊和請求篡改
- 客戶端無需事先獲取token,減少交互
- 支持多種請求方式(GET/POST等)
- 適合第三方API調(diào)用場景
缺點:
- 實現(xiàn)復雜,客戶端和服務端需要一致的簽名算法
- 調(diào)試困難,簽名錯誤不易排查
- 需要安全地管理密鑰
- 計算簽名有一定性能開銷
六、分布式鎖防重復提交
6.1 基本原理
分布式鎖是一種常用的并發(fā)控制機制,可以用來防止重復提交。
當收到請求時,系統(tǒng)嘗試獲取一個基于請求特征(如用戶ID+操作類型)的分布式鎖,如果獲取成功則處理請求,否則拒絕請求。
這種方式特別適合防止用戶在短時間內(nèi)多次點擊提交按鈕導致的重復提交問題。
6.2 SpringBoot實現(xiàn)
首先,添加Redis依賴并配置Redisson客戶端:
@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient(RedisProperties redisProperties) { Config config = new Config(); String redisUrl = String.format("redis://%s:%d", redisProperties.getHost(), redisProperties.getPort()); config.useSingleServer() .setAddress(redisUrl) .setPassword(redisProperties.getPassword()) .setDatabase(redisProperties.getDatabase()); return Redisson.create(config); } }
創(chuàng)建分布式鎖服務:
@Service @Slf4j public class DistributedLockService { @Autowired private RedissonClient redissonClient; /** * 嘗試獲取鎖 * @param lockKey 鎖的鍵 * @param waitTime 等待獲取鎖的最長時間 * @param leaseTime 持有鎖的時間 * @return 是否獲取到鎖 */ public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) { RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Thread interrupted while trying to acquire lock", e); return false; } } /** * 釋放鎖 */ public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
創(chuàng)建防重復提交注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PreventDuplicateSubmit { /** * 鎖的前綴 */ String prefix() default "duplicate_check:"; /** * 等待獲取鎖的時間(毫秒) */ long waitTime() default 0; /** * 持有鎖的時間(毫秒) */ long leaseTime() default 5000; /** * 鎖的Key的SpEL表達式 */ String key() default ""; }
創(chuàng)建防重復提交切面:
@Aspect @Component @Slf4j public class DuplicateSubmitAspect { @Autowired private DistributedLockService lockService; @Around("@annotation(preventDuplicateSubmit)") public Object around(ProceedingJoinPoint point, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable { // 獲取請求信息 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { return point.proceed(); } HttpServletRequest request = attributes.getRequest(); String requestURI = request.getRequestURI(); String requestMethod = request.getMethod(); // 獲取當前用戶ID(實際項目中應從認證信息獲?。? String userId = getUserId(request); // 構建鎖的key String lockKey; if (StringUtils.isNotEmpty(preventDuplicateSubmit.key())) { // 使用SpEL表達式解析key StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("request", request); // 添加方法參數(shù)到上下文 MethodSignature signature = (MethodSignature) point.getSignature(); String[] parameterNames = signature.getParameterNames(); Object[] args = point.getArgs(); for (int i = 0; i < parameterNames.length; i++) { context.setVariable(parameterNames[i], args[i]); } Expression expression = new SpelExpressionParser().parseExpression(preventDuplicateSubmit.key()); lockKey = preventDuplicateSubmit.prefix() + expression.getValue(context, String.class); } else { // 默認使用用戶ID + URI + 方法作為鎖key lockKey = preventDuplicateSubmit.prefix() + userId + ":" + requestURI + ":" + requestMethod; } // 嘗試獲取鎖 boolean locked = lockService.tryLock(lockKey, preventDuplicateSubmit.waitTime(), preventDuplicateSubmit.leaseTime(), TimeUnit.MILLISECONDS); if (!locked) { log.warn("Duplicate submit detected. userId: {}, uri: {}", userId, requestURI); throw new DuplicateSubmitException("請勿重復提交"); } try { // 執(zhí)行實際方法 return point.proceed(); } finally { // 釋放鎖 lockService.unlock(lockKey); } } private String getUserId(HttpServletRequest request) { // 實際項目中應從認證信息獲取用戶ID // 這里簡化處理,從請求頭或會話中獲取 String userId = request.getHeader("X-User-Id"); if (StringUtils.isEmpty(userId)) { // 如果請求頭中沒有,嘗試從會話中獲取 HttpSession session = request.getSession(false); if (session != null) { Object userObj = session.getAttribute("user"); if (userObj != null) { // 假設user對象有getId方法 // userId = ((User) userObj).getId(); userId = "demo-user"; } } } // 如果仍然沒有用戶ID,使用IP地址作為標識 if (StringUtils.isEmpty(userId)) { userId = request.getRemoteAddr(); } return userId; } }
創(chuàng)建異常類:
public class DuplicateSubmitException extends RuntimeException { public DuplicateSubmitException(String message) { super(message); } }
創(chuàng)建全局異常處理:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(DuplicateSubmitException.class) public ResponseEntity<Map<String, String>> handleDuplicateSubmitException(DuplicateSubmitException e) { Map<String, String> error = new HashMap<>(); error.put("error", "duplicate_submit"); error.put("message", e.getMessage()); return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(error); } }
在需要防重復提交的接口上使用注解:
@RestController @RequestMapping("/api/orders") public class OrderController { @PostMapping @PreventDuplicateSubmit(leaseTime = 5000) public ResponseEntity<?> submitOrder(@RequestBody OrderRequest request) { // 處理訂單提交 return ResponseEntity.ok(Map.of("orderId", "123456")); } @PostMapping("/complex") @PreventDuplicateSubmit(key = "#request.productId + ':' + #request.userId") public ResponseEntity<?> submitComplexOrder(@RequestBody OrderRequest request) { // 處理復雜訂單提交 return ResponseEntity.ok(Map.of("orderId", "123456")); } }
6.3 優(yōu)缺點分析
優(yōu)點:
- 可以有效防止短時間內(nèi)的重復提交
- 支持基于業(yè)務屬性的鎖定,靈活性高
- 使用簡單,只需添加注解
- 可以與業(yè)務邏輯解耦
缺點:
- 依賴外部分布式鎖系統(tǒng)
- 鎖的粒度和超時時間需要仔細設計
- 可能導致正常請求被誤判為重復提交
- 需要正確處理鎖的釋放,避免死鎖
七、方案對比
防重放方案 | 安全級別 | 實現(xiàn)復雜度 | 性能影響 | 分布式支持 | 客戶端配合 | 適用場景 |
---|---|---|---|---|---|---|
時間戳+超時機制 | 低 | 簡單 | 低 | 好 | 簡單 | 一般API,低安全需求 |
Nonce+Redis緩存 | 高 | 中等 | 中 | 好 | 中等 | 安全敏感API |
冪等性令牌機制 | 高 | 中等 | 中 | 好 | 復雜 | 非冪等操作,如支付 |
請求簽名認證 | 極高 | 復雜 | 中高 | 好 | 復雜 | 第三方API,金融接口 |
分布式鎖防重復提交 | 中 | 中等 | 中 | 好 | 無需 | 表單提交,用戶操作 |
八、總結
在實際應用中,往往需要組合使用多種防重放策略,實施分層防護,并與業(yè)務邏輯緊密結合,才能構建出既安全又易用的系統(tǒng)。
防重放攻擊只是Web安全的一個方面,還應關注其他安全威脅,如XSS、CSRF、SQL注入等,綜合提升系統(tǒng)的安全性。
以上就是SpringBoot實現(xiàn)防重放攻擊的五種方案的詳細內(nèi)容,更多關于SpringBoot防重放攻擊的資料請關注腳本之家其它相關文章!
相關文章
Java實現(xiàn)LeetCode(1.兩數(shù)之和)
這篇文章主要介紹了Java實現(xiàn)LeetCode(兩數(shù)之和),本文使用java采用多種發(fā)放實現(xiàn)了LeetCode的兩數(shù)之和題目,需要的朋友可以參考下2021-06-06SpringBoot實現(xiàn)Logback輸出日志到Kafka方式
本文介紹了如何在SpringBoot應用中通過自定義Appender實現(xiàn)Logback輸出日志到Kafka,包括配置maven依賴、Kafka工具類和logback.xml配置2025-02-02