亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

SpringBoot實現(xiàn)防重放攻擊的五種方案

 更新時間:2025年06月16日 08:45:14   作者:風象南  
在項目開發(fā)中,重放攻擊(Replay?Attack)是一種常見的攻擊手段,攻擊者截獲有效請求后重新發(fā)送給服務器,以達到未授權訪問、重復交易或身份冒充等目的,本文將介紹五種在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中的executeQuery()方法使用

    java中的executeQuery()方法使用

    這篇文章主要介紹了java中的executeQuery()方法使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • 淺聊一下Spring?Security的使用方法

    淺聊一下Spring?Security的使用方法

    Spring?Security?是一個基于?Spring?框架的安全框架,提供了一套安全性認證和授權的解決方案,用于保護?Web?應用程序和服務,接下來小編就和大家聊聊Spring?Security,感興趣的小伙伴跟著小編一起來看看吧
    2023-08-08
  • Java實現(xiàn)LeetCode(1.兩數(shù)之和)

    Java實現(xiàn)LeetCode(1.兩數(shù)之和)

    這篇文章主要介紹了Java實現(xiàn)LeetCode(兩數(shù)之和),本文使用java采用多種發(fā)放實現(xiàn)了LeetCode的兩數(shù)之和題目,需要的朋友可以參考下
    2021-06-06
  • Java線程池必知必會知識點總結

    Java線程池必知必會知識點總結

    這篇文章主要給大家介紹了關于Java線程池必知必會知識點的相關資料,文中通過圖文以及實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2022-02-02
  • SpringBoot實現(xiàn)Logback輸出日志到Kafka方式

    SpringBoot實現(xiàn)Logback輸出日志到Kafka方式

    本文介紹了如何在SpringBoot應用中通過自定義Appender實現(xiàn)Logback輸出日志到Kafka,包括配置maven依賴、Kafka工具類和logback.xml配置
    2025-02-02
  • 淺談Java分布式架構下如何實現(xiàn)分布式鎖

    淺談Java分布式架構下如何實現(xiàn)分布式鎖

    這篇文章主要介紹了淺談Java分布式架構下如何實現(xiàn)分布式鎖,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-07-07
  • java實現(xiàn)登錄之后抓取數(shù)據(jù)

    java實現(xiàn)登錄之后抓取數(shù)據(jù)

    這篇文章給大家分享了用JAVA實現(xiàn)在登陸以后抓取網(wǎng)站的數(shù)據(jù)的相關知識,有興趣的朋友可以測試參考下。
    2018-07-07
  • mybatis如何實現(xiàn)繼承映射

    mybatis如何實現(xiàn)繼承映射

    這篇文章主要介紹了mybatis如何實現(xiàn)繼承映射的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • Java利用Request請求獲取IP地址的方法詳解

    Java利用Request請求獲取IP地址的方法詳解

    在開發(fā)中我們經(jīng)常需要獲取用戶IP地址,通過地址來實現(xiàn)一些功能,下面這篇文章主要給大家介紹了關于Java利用Request請求獲取IP地址的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。
    2017-10-10
  • 基于注解的組件掃描詳解

    基于注解的組件掃描詳解

    這篇文章主要介紹了基于注解的組件掃描詳解,具有一定借鑒價值,需要的朋友可以參考下。
    2017-12-12

最新評論