欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2025年06月16日 08:45:14   作者:風(fēng)象南  
在項(xiàng)目開(kāi)發(fā)中,重放攻擊(Replay?Attack)是一種常見(jiàn)的攻擊手段,攻擊者截獲有效請(qǐng)求后重新發(fā)送給服務(wù)器,以達(dá)到未授權(quán)訪問(wèn)、重復(fù)交易或身份冒充等目的,本文將介紹五種在SpringBoot應(yīng)用中實(shí)現(xiàn)防重放攻擊的方案,需要的朋友可以參考下

一、重放攻擊基本概念

1.1 什么是重放攻擊

重放攻擊是一種網(wǎng)絡(luò)攻擊手段,攻擊者截獲一個(gè)有效的數(shù)據(jù)傳輸,然后在稍后的時(shí)間重新發(fā)送相同的數(shù)據(jù),以實(shí)現(xiàn)欺騙系統(tǒng)的目的。在Web應(yīng)用中,這通常表現(xiàn)為重復(fù)提交相同的請(qǐng)求,比如:

  • 重復(fù)提交訂單付款請(qǐng)求
  • 重復(fù)使用過(guò)期的訪問(wèn)令牌
  • 重復(fù)提交表單數(shù)據(jù)
  • 重新發(fā)送包含認(rèn)證信息的請(qǐng)求

1.2 重放攻擊的危害

重放攻擊可能導(dǎo)致以下安全問(wèn)題:

  • 資金損失:重復(fù)執(zhí)行支付交易
  • 資源耗盡:大量重復(fù)請(qǐng)求導(dǎo)致系統(tǒng)資源枯竭
  • 數(shù)據(jù)不一致:重復(fù)提交導(dǎo)致數(shù)據(jù)重復(fù)或狀態(tài)混亂
  • 業(yè)務(wù)邏輯被繞過(guò):繞過(guò)設(shè)計(jì)中的業(yè)務(wù)規(guī)則
  • 權(quán)限提升:復(fù)用他人有效的認(rèn)證信息

二、時(shí)間戳+請(qǐng)求超時(shí)機(jī)制

2.1 基本原理

這種方案要求客戶端在每個(gè)請(qǐng)求中附帶當(dāng)前時(shí)間戳,服務(wù)器收到請(qǐng)求后,檢查時(shí)間戳是否在允許的時(shí)間窗口內(nèi)(通常為幾分鐘)。

如果請(qǐng)求的時(shí)間戳超出時(shí)間窗口,則認(rèn)為是過(guò)期請(qǐng)求或潛在的重放攻擊,拒絕處理該請(qǐng)求。

2.2 SpringBoot實(shí)現(xiàn)

首先,創(chuàng)建一個(gè)請(qǐng)求包裝類(lèi),包含時(shí)間戳字段:

@Data
public class ApiRequest<T> {
    private Long timestamp;  // 請(qǐng)求時(shí)間戳,毫秒級(jí)
    private T data;          // 實(shí)際請(qǐng)求數(shù)據(jù)
}

然后,創(chuàng)建一個(gè)攔截器來(lái)檢查請(qǐng)求時(shí)間戳:

@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分鐘時(shí)間窗口
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        // 檢查是否需要進(jìn)行時(shí)間戳驗(yàn)證
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if (!handlerMethod.getMethod().isAnnotationPresent(CheckTimestamp.class)) {
            return true;
        }
        
        // 獲取請(qǐng)求體
        String requestBody = getRequestBody(request);
        
        try {
            // 解析請(qǐng)求體,獲取時(shí)間戳
            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;
            }
            
            // 檢查時(shí)間戳是否在允許的時(shí)間窗口內(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 {
        // 針對(duì)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());
            }
        }
        
        // 針對(duì)MultiReadHttpServletRequest的處理
        try (BufferedReader reader = request.getReader()) {
            return reader.lines().collect(Collectors.joining(System.lineSeparator()));
        }
    }
}

創(chuàng)建一個(gè)注解,用于標(biāo)記需要進(jìn)行時(shí)間戳驗(yàn)證的接口:

@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)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 實(shí)現(xiàn)簡(jiǎn)單,無(wú)需額外存儲(chǔ)
  • 不依賴會(huì)話狀態(tài),適合分布式系統(tǒng)
  • 客戶端實(shí)現(xiàn)簡(jiǎn)單,只需添加時(shí)間戳

缺點(diǎn):

  • 需要客戶端和服務(wù)器時(shí)間同步
  • 時(shí)間窗口存在權(quán)衡:太短影響用戶體驗(yàn),太長(zhǎng)降低安全性
  • 無(wú)法防止時(shí)間窗口內(nèi)的重放攻擊
  • 不適合時(shí)間敏感的高安全場(chǎng)景

三、Nonce隨機(jī)數(shù)+Redis緩存

3.1 基本原理

Nonce(Number used once)是一個(gè)只使用一次的隨機(jī)數(shù)。在此方案中,客戶端每次請(qǐng)求都生成一個(gè)唯一的隨機(jī)數(shù),并發(fā)送給服務(wù)器。

服務(wù)器將使用過(guò)的Nonce存儲(chǔ)在Redis緩存中一段時(shí)間,拒絕任何使用重復(fù)Nonce的請(qǐng)求。這種方式可以有效防止重放攻擊,因?yàn)槊總€(gè)有效請(qǐng)求都需要一個(gè)從未使用過(guò)的Nonce。

3.2 SpringBoot實(shí)現(xiàn)

首先,擴(kuò)展請(qǐng)求包裝類(lèi),添加Nonce字段:

@Data
public class ApiRequest<T> {
    private Long timestamp;    // 請(qǐng)求時(shí)間戳,毫秒級(jí)
    private String nonce;      // 隨機(jī)數(shù),每次請(qǐng)求唯一
    private T data;            // 實(shí)際請(qǐng)求數(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小時(shí)
    
    @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;
            }
            
            // 檢查時(shí)間戳
            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)建注解,標(biāo)記需要進(jìn)行Nonce驗(yàn)證的接口:

@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)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 安全性高,每個(gè)請(qǐng)求都必須使用唯一的Nonce
  • 可以有效防止在任何時(shí)間窗口內(nèi)的重放攻擊
  • 結(jié)合時(shí)間戳可以雙重保障

缺點(diǎn):

  • 需要存儲(chǔ)使用過(guò)的Nonce,增加系統(tǒng)復(fù)雜性
  • 在分布式系統(tǒng)中需要共享Nonce存儲(chǔ)
  • 對(duì)Redis等存儲(chǔ)系統(tǒng)有依賴
  • 客戶端需要生成唯一Nonce,實(shí)現(xiàn)相對(duì)復(fù)雜

四、冪等性令牌機(jī)制

4.1 基本原理

冪等性令牌機(jī)制是一種專(zhuān)門(mén)針對(duì)非冪等操作(如創(chuàng)建訂單、支付等)設(shè)計(jì)的防重放方案。

服務(wù)器先生成一個(gè)一次性的令牌并提供給客戶端,客戶端在執(zhí)行操作時(shí)必須提交這個(gè)令牌,服務(wù)器驗(yàn)證令牌有效后執(zhí)行操作并立即使令牌失效,從而保證操作不會(huì)重復(fù)執(zhí)行。

4.2 SpringBoot實(shí)現(xiàn)

首先,創(chuàng)建令牌服務(wù):

@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;
    }
    
    /**
     * 驗(yàn)證并消費(fèi)令牌
     * @return true表示令牌有效且已成功消費(fèi),false表示令牌無(wú)效或已被消費(fèi)
     */
    public boolean validateAndConsumeToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        
        String key = TOKEN_PREFIX + token;
        // 使用Redis的原子操作驗(yàn)證并更新令牌狀態(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;
        }
        
        // 從請(qǐng)求頭獲取冪等性令牌
        String token = request.getHeader("Idempotency-Token");
        if (StringUtils.isEmpty(token)) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("Missing idempotency token");
            return false;
        }
        
        // 驗(yàn)證并消費(fèi)令牌
        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();

// 第二步: 使用令牌提交請(qǐng)求
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)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 專(zhuān)為非冪等操作設(shè)計(jì),安全性高
  • 客戶端必須先獲取令牌,可以有效防止未授權(quán)請(qǐng)求
  • 服務(wù)端控制令牌生成和驗(yàn)證,安全可控
  • 可以與業(yè)務(wù)邏輯完美結(jié)合

缺點(diǎn):

  • 需要額外的獲取令牌請(qǐng)求,增加交互復(fù)雜性
  • 依賴外部存儲(chǔ)系統(tǒng)保存令牌狀態(tài)
  • 對(duì)客戶端有特定要求,實(shí)現(xiàn)相對(duì)復(fù)雜
  • 令牌有效期管理需要權(quán)衡

五、請(qǐng)求簽名認(rèn)證

5.1 基本原理

請(qǐng)求簽名認(rèn)證方案通過(guò)對(duì)請(qǐng)求參數(shù)、時(shí)間戳、隨機(jī)數(shù)等信息進(jìn)行加密簽名,確保請(qǐng)求在傳輸過(guò)程中不被篡改,同時(shí)結(jié)合時(shí)間戳和隨機(jī)數(shù)防止重放攻擊。

該方案通常用于API安全性要求較高的場(chǎng)景,如支付、金融等領(lǐng)域。

5.2 SpringBoot實(shí)現(xiàn)

首先,創(chuàng)建請(qǐng)求簽名工具類(lèi):

@Component
public class SignatureUtils {
    
    /**
     * 生成簽名
     * @param params 參與簽名的參數(shù)
     * @param timestamp 時(shí)間戳
     * @param nonce 隨機(jī)數(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. 構(gòu)建簽名字符串
        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í)間戳和隨機(jī)數(shù)
        builder.append("timestamp=").append(timestamp).append("&");
        builder.append("nonce=").append(nonce).append("&");
        builder.append("key=").append(secretKey);
        
        // 4. 進(jìn)行MD5簽名
        return DigestUtils.md5DigestAsHex(builder.toString().getBytes(StandardCharsets.UTF_8));
    }
    
    /**
     * 驗(yàn)證簽名
     */
    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)建簽名驗(yàn)證攔截器:

@Component
@Slf4j
public class SignatureInterceptor implements HandlerInterceptor {
    
    private static final long ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分鐘時(shí)間窗口
    
    @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. 獲取請(qǐng)求頭中的簽名信息
            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. 檢查時(shí)間戳是否在允許的時(shí)間窗口內(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. 獲取請(qǐng)求參數(shù)
            Map<String, String> params = new HashMap<>();
            // 從請(qǐng)求體或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 {
                // 解析請(qǐng)求體,這里簡(jiǎn)化處理
                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. 驗(yàn)證簽名
            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)建簽名驗(yàn)證注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckSignature {
}

在需要簽名驗(yàn)證的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. 準(zhǔn)備簽名參數(shù)
        long timestamp = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        
        // 2. 請(qǐng)求參數(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ā)起請(qǐng)求
        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);
        
        // 設(shè)置請(qǐng)求體...
        
        // 獲取響應(yīng)...
        
        return "Response";
    }
    
    private static String generateSignature(Map<String, String> params, long timestamp, String nonce, String secretKey) {
        // 實(shí)現(xiàn)與服務(wù)端相同的簽名算法
        // ...
    }
}

5.4 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 安全性高,可以同時(shí)防止重放攻擊和請(qǐng)求篡改
  • 客戶端無(wú)需事先獲取token,減少交互
  • 支持多種請(qǐng)求方式(GET/POST等)
  • 適合第三方API調(diào)用場(chǎng)景

缺點(diǎn):

  • 實(shí)現(xiàn)復(fù)雜,客戶端和服務(wù)端需要一致的簽名算法
  • 調(diào)試?yán)щy,簽名錯(cuò)誤不易排查
  • 需要安全地管理密鑰
  • 計(jì)算簽名有一定性能開(kāi)銷(xiāo)

六、分布式鎖防重復(fù)提交

6.1 基本原理

分布式鎖是一種常用的并發(fā)控制機(jī)制,可以用來(lái)防止重復(fù)提交。

當(dāng)收到請(qǐng)求時(shí),系統(tǒng)嘗試獲取一個(gè)基于請(qǐng)求特征(如用戶ID+操作類(lèi)型)的分布式鎖,如果獲取成功則處理請(qǐng)求,否則拒絕請(qǐng)求。

這種方式特別適合防止用戶在短時(shí)間內(nèi)多次點(diǎn)擊提交按鈕導(dǎo)致的重復(fù)提交問(wèn)題。

6.2 SpringBoot實(shí)現(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)建分布式鎖服務(wù):

@Service
@Slf4j
public class DistributedLockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 嘗試獲取鎖
     * @param lockKey 鎖的鍵
     * @param waitTime 等待獲取鎖的最長(zhǎng)時(shí)間
     * @param leaseTime 持有鎖的時(shí)間
     * @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)建防重復(fù)提交注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    /**
     * 鎖的前綴
     */
    String prefix() default "duplicate_check:";
    
    /**
     * 等待獲取鎖的時(shí)間(毫秒)
     */
    long waitTime() default 0;
    
    /**
     * 持有鎖的時(shí)間(毫秒)
     */
    long leaseTime() default 5000;
    
    /**
     * 鎖的Key的SpEL表達(dá)式
     */
    String key() default "";
}

創(chuàng)建防重復(fù)提交切面:

@Aspect
@Component
@Slf4j
public class DuplicateSubmitAspect {
    
    @Autowired
    private DistributedLockService lockService;
    
    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint point, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        // 獲取請(qǐng)求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return point.proceed();
        }
        
        HttpServletRequest request = attributes.getRequest();
        String requestURI = request.getRequestURI();
        String requestMethod = request.getMethod();
        
        // 獲取當(dāng)前用戶ID(實(shí)際項(xiàng)目中應(yīng)從認(rèn)證信息獲?。?
        String userId = getUserId(request);
        
        // 構(gòu)建鎖的key
        String lockKey;
        if (StringUtils.isNotEmpty(preventDuplicateSubmit.key())) {
            // 使用SpEL表達(dá)式解析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 {
            // 默認(rèn)使用用戶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("請(qǐng)勿重復(fù)提交");
        }
        
        try {
            // 執(zhí)行實(shí)際方法
            return point.proceed();
        } finally {
            // 釋放鎖
            lockService.unlock(lockKey);
        }
    }
    
    private String getUserId(HttpServletRequest request) {
        // 實(shí)際項(xiàng)目中應(yīng)從認(rèn)證信息獲取用戶ID
        // 這里簡(jiǎn)化處理,從請(qǐng)求頭或會(huì)話中獲取
        String userId = request.getHeader("X-User-Id");
        if (StringUtils.isEmpty(userId)) {
            // 如果請(qǐng)求頭中沒(méi)有,嘗試從會(huì)話中獲取
            HttpSession session = request.getSession(false);
            if (session != null) {
                Object userObj = session.getAttribute("user");
                if (userObj != null) {
                    // 假設(shè)user對(duì)象有g(shù)etId方法
                    // userId = ((User) userObj).getId();
                    userId = "demo-user";
                }
            }
        }
        
        // 如果仍然沒(méi)有用戶ID,使用IP地址作為標(biāo)識(shí)
        if (StringUtils.isEmpty(userId)) {
            userId = request.getRemoteAddr();
        }
        
        return userId;
    }
}

創(chuàng)建異常類(lèi):

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);
    }
}

在需要防重復(fù)提交的接口上使用注解:

@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) {
        // 處理復(fù)雜訂單提交
        return ResponseEntity.ok(Map.of("orderId", "123456"));
    }
}

6.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 可以有效防止短時(shí)間內(nèi)的重復(fù)提交
  • 支持基于業(yè)務(wù)屬性的鎖定,靈活性高
  • 使用簡(jiǎn)單,只需添加注解
  • 可以與業(yè)務(wù)邏輯解耦

缺點(diǎn):

  • 依賴外部分布式鎖系統(tǒng)
  • 鎖的粒度和超時(shí)時(shí)間需要仔細(xì)設(shè)計(jì)
  • 可能導(dǎo)致正常請(qǐng)求被誤判為重復(fù)提交
  • 需要正確處理鎖的釋放,避免死鎖

七、方案對(duì)比

防重放方案安全級(jí)別實(shí)現(xiàn)復(fù)雜度性能影響分布式支持客戶端配合適用場(chǎng)景
時(shí)間戳+超時(shí)機(jī)制簡(jiǎn)單簡(jiǎn)單一般API,低安全需求
Nonce+Redis緩存中等中等安全敏感API
冪等性令牌機(jī)制中等復(fù)雜非冪等操作,如支付
請(qǐng)求簽名認(rèn)證極高復(fù)雜中高復(fù)雜第三方API,金融接口
分布式鎖防重復(fù)提交中等無(wú)需表單提交,用戶操作

八、總結(jié)

在實(shí)際應(yīng)用中,往往需要組合使用多種防重放策略,實(shí)施分層防護(hù),并與業(yè)務(wù)邏輯緊密結(jié)合,才能構(gòu)建出既安全又易用的系統(tǒng)。

防重放攻擊只是Web安全的一個(gè)方面,還應(yīng)關(guān)注其他安全威脅,如XSS、CSRF、SQL注入等,綜合提升系統(tǒng)的安全性。

以上就是SpringBoot實(shí)現(xiàn)防重放攻擊的五種方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot防重放攻擊的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • java中的executeQuery()方法使用

    java中的executeQuery()方法使用

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

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

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

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

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

    Java線程池必知必會(huì)知識(shí)點(diǎn)總結(jié)

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

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

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

    淺談Java分布式架構(gòu)下如何實(shí)現(xiàn)分布式鎖

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

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

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

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

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

    Java利用Request請(qǐng)求獲取IP地址的方法詳解

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

    基于注解的組件掃描詳解

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

最新評(píng)論