成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

SpringBoot 接口防刷的五種方案,太強了!

開發 前端
接口防刷是一個系統性工程,需要綜合考慮安全性、用戶體驗、性能開銷和運維復雜度等多方面因素。本文介紹的 5 種方案各有優缺點,你可以根據實際需求靈活選擇和組合。

在當今的互聯網環境中,接口防刷已成為保障系統安全與穩定運行的關鍵環節。惡意的高頻請求如同隱藏在暗處的“殺手”,不僅會大量消耗服務器寶貴的資源,還可能引發數據異常,嚴重時甚至會導致整個系統癱瘓,給業務帶來不可估量的損失。

本文將深入剖析在 Spring Boot 框架下實現接口防刷的 5 種技術方案,幫助你根據實際需求選擇最合適的方法,為系統安全保駕護航。

1. 基于注解的訪問頻率限制

基于注解的訪問頻率限制是一種廣受歡迎的接口防刷方案,它通過自定義注解和 AOP 切面的巧妙結合,實現了對接口訪問頻率的有效控制。這種方法簡單易用,實現成本低,就像給接口穿上了一層輕便的“防護衣”。

實現步驟

1.1 創建限流注解

首先,我們需要創建一個自定義的限流注解,用于標記需要進行訪問頻率限制的接口方法。以下是具體的代碼實現:

圖片圖片

在這個注解中,我們可以通過 time 屬性設置限制的時間段,通過 count 屬性設置在該時間段內允許的最大請求次數,通過 key 屬性指定限流的鍵,支持使用 SpEL 表達式進行靈活配置,message 屬性則用于在請求被限定時給用戶提供提示信息。

1.2 實現限流切面

接下來,我們需要實現一個 AOP 切面,用于處理限流邏輯。以下是具體的代碼實現:

@Aspect
@Component
@Slf4j
publicclass RateLimitAspect {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        // 獲取請求的方法名
        String methodName = pjp.getSignature().getName();
        // 獲取請求的類名
        String className = pjp.getTarget().getClass().getName();
        
        // 組合限流 key
        String limitKey = getLimitKey(pjp, rateLimit, methodName, className);
        
        // 獲取限流參數
        int time = rateLimit.time();
        int count = rateLimit.count();
        
        // 執行限流邏輯
        boolean limited = isLimited(limitKey, time, count);
        if (limited) {
            thrownew RuntimeException(rateLimit.message());
        }
        
        // 執行目標方法
        return pjp.proceed();
    }
    
    private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {
        // 獲取用戶自定義的 key
        String key = rateLimit.key();
        
        if (StringUtils.hasText(key)) {
            // 支持 SpEL 表達式解析
            StandardEvaluationContext context = new StandardEvaluationContext();
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            String[] parameterNames = signature.getParameterNames();
            Object[] args = pjp.getArgs();
            
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
            
            ExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(key);
            key = expression.getValue(context, String.class);
        } else {
            // 默認使用類名+方法名+IP 地址作為 key
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ip = getIpAddress(request);
            key = ip + ":" + className + ":" + methodName;
        }
        
        return"rate_limit:" + key;
    }
    
    private boolean isLimited(String key, int time, int count) {
        // 使用 Redis 的計數器實現限流
        try {
            Long currentCount = redisTemplate.opsForValue().increment(key, 1);
            
            // 如果是第一次訪問,設置過期時間
            if (currentCount == 1) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            
            return currentCount > count;
        } catch (Exception e) {
            log.error("限流異常", e);
            returnfalse;
        }
    }
    
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

在這個切面中,我們使用 @Around 注解攔截所有標記了 @RateLimit 注解的方法。在方法執行前,我們會根據注解中的參數計算出限流的鍵,并使用 Redis 的計數器來記錄請求次數。如果請求次數超過了限制,我們會拋出一個異常,提示用戶操作過于頻繁。

1.3 使用示例

以下是一個使用 @RateLimit 注解的示例:

圖片圖片

在這個示例中,我們在 getUser 方法上使用了 @RateLimit 注解,設置了 60 秒內最多允許 3 次請求。在 updateUser 方法上,我們使用了 SpEL 表達式來動態生成限流的鍵,實現了更加靈活的限流策略。

優缺點分析

優點

  • 實現簡單,上手容易:該方案的實現邏輯清晰,代碼量較少,即使是初學者也能快速掌握。在單機情況下,甚至可以去掉 Redis 換成本地緩存實現,進一步降低了實現難度。
  • 注解式使用,對業務代碼無侵入:通過自定義注解的方式,我們可以將限流邏輯與業務邏輯分離,對業務代碼的改動非常小,不會影響原有的業務功能。
  • 可以精確控制接口粒度:我們可以針對不同的接口方法設置不同的限流參數,實現對接口訪問頻率的精確控制。
  • 支持靈活的限流策略配置:通過 SpEL 表達式,我們可以根據請求的參數、用戶信息等動態生成限流的鍵,實現更加靈活的限流策略。

缺點

  • 限流邏輯相對簡單,無法應對復雜場景:該方案的限流邏輯主要基于固定的時間窗口和請求次數,對于一些復雜的場景,如突發流量、動態限流等,可能無法滿足需求。
  • 缺少預警機制:當請求次數接近或達到限制時,系統無法及時發出預警,可能會導致用戶體驗下降。

2. 令牌桶算法實現限流

令牌桶算法是一種更加靈活的限流算法,它就像一個裝有令牌的桶,系統會以固定的速率向桶中添加令牌,每個請求需要從桶中獲取一個令牌才能被處理。這種算法可以允許突發流量,同時又能限制長期的平均流量,為系統提供了更加靈活的流量控制能力。

實現步驟

2.1 引入依賴

Google 提供的 Guava 庫中包含了令牌桶的實現,我們可以通過以下依賴將其引入項目:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2.2 創建令牌桶限流器

接下來,我們需要創建一個令牌桶限流器,用于管理不同接口的令牌桶。以下是具體的代碼實現:

@Component
publicclass RateLimiter {
    // 使用 ConcurrentHashMap 存儲不同接口的令牌桶
    privatefinal ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
    
    /**
     * 獲取特定接口的令牌桶,不存在則創建
     * @param key 限流鍵
     * @param permitsPerSecond 每秒允許的請求量
     * @return 令牌桶實例
     */
    public com.google.common.util.concurrent.RateLimiter getRateLimiter(String key, double permitsPerSecond) {
        return rateLimiterMap.computeIfAbsent(key, 
            k -> com.google.common.util.concurrent.RateLimiter.create(permitsPerSecond));
    }
    
    /**
     * 嘗試獲取令牌
     * @param key 限流鍵
     * @param permitsPerSecond 每秒允許的請求量
     * @param timeout 超時時間
     * @param unit 時間單位
     * @return 是否獲取成功
     */
    public boolean tryAcquire(String key, double permitsPerSecond, long timeout, TimeUnit unit) {
        com.google.common.util.concurrent.RateLimiter rateLimiter = getRateLimiter(key, permitsPerSecond);
        return rateLimiter.tryAcquire(1, timeout, unit);
    }
}

在這個限流器中,我們使用 ConcurrentHashMap 來存儲不同接口的令牌桶,確保線程安全。通過 getRateLimiter 方法,我們可以根據限流鍵獲取對應的令牌桶,如果令牌桶不存在,則會自動創建一個新的令牌桶。通過 tryAcquire 方法,我們可以嘗試從令牌桶中獲取一個令牌,如果在指定的超時時間內獲取成功,則返回 true,否則返回 false

2.3 創建攔截器

為了實現對接口的限流,我們需要創建一個攔截器,在請求進入接口之前進行令牌的獲取操作。以下是具體的代碼實現:

圖片圖片

在這個攔截器中,我們首先判斷請求的 URI 是否以 /api/ 開頭,如果是,則進行限流處理。然后,我們獲取請求的 IP 地址和 URI,組合成限流鍵。接著,我們嘗試從令牌桶中獲取一個令牌,如果獲取失敗,則返回一個限流響應,提示用戶請求過于頻繁。

2.4 配置攔截器

最后,我們需要將攔截器配置到 Spring Boot 應用中,使其生效。以下是具體的代碼實現:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private TokenBucketInterceptor tokenBucketInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenBucketInterceptor)
                .addPathPatterns("/**");
    }
}

在這個配置類中,我們使用 WebMvcConfigurer 接口的 addInterceptors 方法將攔截器添加到攔截器鏈中,并設置攔截所有的請求。

優缺點分析

優點

  • 支持突發流量,不會完全拒絕短時高峰:令牌桶算法允許在短時間內有較高的請求速率,只要桶中有足夠的令牌。當突發流量到來時,系統可以快速處理這些請求,而不會像固定窗口算法那樣直接拒絕請求。
  • 平滑的限流效果,用戶體驗更好:由于令牌桶算法是以固定的速率向桶中添加令牌,因此可以實現平滑的限流效果,避免了請求的突然中斷,提高了用戶體驗。
  • 可以配置不同接口的不同限流策略:通過使用不同的限流鍵,我們可以為不同的接口配置不同的令牌桶,實現對不同接口的差異化限流。
  • 無需額外的存儲設施:令牌桶算法的實現不需要額外的存儲設施,只需要在內存中維護一個令牌桶即可,降低了系統的復雜度和成本。

缺點

  • 只適用于單機部署,分布式環境需要額外改造:該方案的令牌桶是在內存中維護的,因此只適用于單機部署的環境。在分布式環境中,需要將令牌桶的狀態存儲到共享的存儲設施中,如 Redis,才能實現分布式限流。
  • 重啟應用后狀態丟失:由于令牌桶的狀態是在內存中維護的,因此當應用重啟后,令牌桶的狀態會丟失,需要重新初始化。
  • 無法精確控制時間窗口內的請求總量:令牌桶算法只能控制請求的平均速率,無法精確控制在某個時間窗口內的請求總量。在某些對請求總量有嚴格限制的場景下,可能無法滿足需求。

3. 分布式限流(Redis + Lua 腳本)

在分布式系統中,單機限流方案往往難以滿足需求,因為不同的實例之間無法共享限流狀態。利用 Redis 和 Lua 腳本可以實現高效的分布式限流,確保系統在分布式環境下的安全性和穩定性。

實現步驟

3.1 定義 Lua 腳本

首先,我們需要定義一個 Redis 限流的 Lua 腳本,用于實現限流邏輯。以下是具體的腳本內容:

圖片圖片

在這個腳本中,我們首先獲取限流鍵、限流窗口、限流閾值和當前時間戳。然后,我們移除過期的請求記錄,統計當前窗口內的請求數。如果請求數超過了閾值,我們返回 0 表示拒絕請求。否則,我們添加當前請求記錄,并設置過期時間,最后返回當前窗口剩余可用請求數。

3.2 創建 Redis 限流服務

接下來,我們需要創建一個 Redis 限流服務,用于執行 Lua 腳本并處理限流邏輯。以下是具體的代碼實現:

@Service
@Slf4j
publicclass RedisRateLimiterService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private DefaultRedisScript<Long> rateLimiterScript;
    
    @PostConstruct
    public void init() {
        // 加載 Lua 腳本
        rateLimiterScript = new DefaultRedisScript<>();
        rateLimiterScript.setLocation(new ClassPathResource("scripts/rate_limiter.lua"));
        rateLimiterScript.setResultType(Long.class);
    }
    
    /**
     * 嘗試獲取訪問權限
     * @param key 限流鍵
     * @param window 時間窗口(秒)
     * @param threshold 閾值
     * @return 剩余可用請求數,-1 表示被限流
     */
    public long isAllowed(String key, int window, int threshold) {
        try {
            // 執行 lua 腳本
            List<String> keys = Collections.singletonList(key);
            Long remainingCount = redisTemplate.execute(
                rateLimiterScript, 
                keys, 
                String.valueOf(window), 
                String.valueOf(threshold),
                String.valueOf(System.currentTimeMillis())
            );
            
            return remainingCount == null ? -1 : remainingCount;
        } catch (Exception e) {
            log.error("Redis rate limiter error", e);
            // 發生異常時放行請求
            return threshold;
        }
    }
}

在這個服務中,我們使用 StringRedisTemplate 來執行 Lua 腳本。在 init 方法中,我們加載 Lua 腳本并設置返回值類型。在 isAllowed 方法中,我們執行 Lua 腳本并根據返回值判斷是否允許訪問。如果返回值為 -1 表示被限流,否則返回剩余可用請求數。

3.3 創建分布式限流注解

為了方便使用,我們可以創建一個分布式限流注解,用于標記需要進行分布式限流的接口方法。以下是具體的代碼實現:

圖片圖片

在這個注解中,我們可以通過 prefix 屬性設置限流鍵的前綴,通過 window 屬性設置時間窗口,通過 threshold 屬性設置時間窗口內允許的最大請求數,通過 mode 屬性設置限流模式。

3.4 實現分布式限流切面

接下來,我們需要實現一個 AOP 切面,用于處理分布式限流邏輯。以下是具體的代碼實現:

@Aspect
@Component
@Slf4j
publicclass DistributedRateLimitAspect {
    
    @Autowired
    private RedisRateLimiterService rateLimiterService;
    
    @Autowired(required = false)
    private HttpServletRequest request;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) throws Throwable {
        String key = generateKey(pjp, rateLimit);
        
        long remainingCount = rateLimiterService.isAllowed(
            key, 
            rateLimit.window(), 
            rateLimit.threshold()
        );
        
        if (remainingCount < 0) {
            thrownew RuntimeException("接口訪問過于頻繁,請稍后再試");
        }
        
        // 執行目標方法
        return pjp.proceed();
    }
    
    private String generateKey(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) {
        String methodName = pjp.getSignature().getName();
        String className = pjp.getTarget().getClass().getName();
        StringBuilder key = new StringBuilder(rateLimit.prefix());
        
        key.append(className).append(".").append(methodName);
        
        // 根據限流模式添加不同的后綴
        switch (rateLimit.mode()) {
            case"ip":
                // 按 IP 限流
                key.append(":").append(getIpAddress());
                break;
            case"user":
                // 按用戶限流
                Object userId = getUserId();
                key.append(":").append(userId != null ? userId : "anonymous");
                break;
            case"all":
                // 接口總體限流,不添加后綴
                break;
            default:
                key.append(":").append(getIpAddress());
                break;
        }
        
        return key.toString();
    }
    
    private String getIpAddress() {
        // IP 獲取方法同上
        if (request == null) {
            return"unknown";
        }
        // 獲取 IP 的代碼同上一個示例
        return"127.0.0.1"; // 簡化處理
    }
    
    // 獲取當前用戶 ID,根據實際認證系統實現
    private Object getUserId() {
        // 這里簡化處理,實際中應從認證信息中獲取
        // 例如:SecurityContextHolder.getContext().getAuthentication().getPrincipal()
        returnnull;
    }
}

在這個切面中,我們使用 @Around 注解攔截所有標記了 @DistributedRateLimit 注解的方法。在方法執行前,我們會根據注解中的參數生成限流鍵,并調用 RedisRateLimiterService 的 isAllowed 方法判斷是否允許訪問。如果不允許訪問,我們會拋出一個異常,提示用戶接口訪問過于頻繁。

3.5 使用示例

以下是一個使用 @DistributedRateLimit 注解的示例:

@RestController
@RequestMapping("/api")
publicclass PaymentController {
    
    @DistributedRateLimit(prefix = "pay:", window = 3600, threshold = 5, mode = "user")
    @PostMapping("/payment")
    public Result createPayment(@RequestBody PaymentRequest paymentRequest) {
        // 創建支付業務邏輯
        return paymentService.createPayment(paymentRequest);
    }
    
    @DistributedRateLimit(window = 60, threshold = 30, mode = "ip")
    @GetMapping("/products")
    public List<Product> getProducts() {
        // 查詢產品列表
        return productService.findAll();
    }
    
    @DistributedRateLimit(window = 1, threshold = 100, mode = "all")
    @GetMapping("/hot/resource")
    public Resource getHotResource() {
        // 獲取熱門資源
        return resourceService.getHotResource();
    }
}

在這個示例中,我們在 createPayment 方法上使用了 @DistributedRateLimit 注解,設置了 3600 秒內每個用戶最多允許 5 次請求。在 getProducts 方法上,我們設置了 60 秒內每個 IP 最多允許 30 次請求。在 getHotResource 方法上,我們設置了 1 秒內整個接口最多允許 100 次請求。

優缺點分析

優點

  • 適用于分布式系統,多實例間共享限流狀態:通過使用 Redis 作為共享存儲,不同的實例可以共享限流狀態,確保系統在分布式環境下的一致性和穩定性。
  • 支持多種限流模式:可以根據 IP、用戶、接口總量等不同的維度進行限流,滿足不同場景的需求。
  • 基于滑動窗口,計數更精確:Lua 腳本使用滑動窗口算法來統計請求數,相比固定窗口算法,計數更加精確,可以有效避免誤判。
  • 使用 Lua 腳本保證原子性,避免競態條件:Lua 腳本在 Redis 中是原子執行的,確保了限流邏輯的原子性,避免了多個實例同時修改限流狀態時可能出現的競態條件。

缺點

  • 強依賴 Redis:該方案的實現依賴于 Redis,如果 Redis 出現故障,可能會影響系統的正常運行。
  • 實現復雜度較高:需要編寫 Lua 腳本并進行 Redis 操作,實現復雜度相對較高,對開發人員的技術要求也較高。

4. 集成 Sentinel 實現接口防刷

阿里巴巴開源的 Sentinel 是一個強大的流量控制組件,它就像一個智能的“交通警察”,可以對系統的流量進行實時監控和控制,提供了豐富的限流、熔斷、系統保護等功能。

實現步驟

4.1 添加依賴

首先,我們需要在項目中添加 Sentinel 的依賴:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.4.0</version>
</dependency>

4.2 配置 Sentinel

在 application.properties 中添加 Sentinel 的配置:

# Sentinel 控制臺地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
# 取消 Sentinel 控制臺懶加載
spring.cloud.sentinel.eager=true
# 應用名稱
spring.application.name=my-application

在這個配置中,我們設置了 Sentinel 控制臺的地址,取消了控制臺的懶加載,并指定了應用的名稱。

4.3 創建 Sentinel 配置

接下來,我們需要創建一個 Sentinel 配置類,用于定義流控規則。以下是具體的代碼實現:

圖片圖片

在這個配置類中,我們創建了一個 SentinelResourceAspect 實例,并在 init 方法中定義了流控規則。對于 /api/user 接口,我們設置了每秒允許 10 個請求的限流規則。對于 /api/order 接口,我們設置了每秒允許 5 個請求的限流規則,并開啟了預熱模式,預熱期為 10 秒。

4.4 創建 URL 資源解析器

為了讓 Sentinel 能夠正確識別請求的資源,我們需要創建一個 URL 資源解析器。以下是具體的代碼實現:

@Component
publicclass UrlCleaner implements RequestOriginParser {
    
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 獲取請求的 URL 路徑
        String path = request.getRequestURI();
        
        // 可以添加更復雜的解析邏輯,例如:
        // 1. 去除路徑變量:/api/user/123 -> /api/user/{id}
        // 2. 添加請求方法前綴:GET:/api/user
        
        return path;
    }
}

在這個解析器中,我們簡單地返回請求的 URL 路徑,你可以根據實際需求添加更復雜的解析邏輯。

4.5 創建全局異常處理器

當請求被 Sentinel 限流時,會拋出 BlockException 異常,我們需要創建一個全局異常處理器來處理這個異常。以下是具體的代碼實現:

@RestControllerAdvice
publicclass SentinelExceptionHandler {
    
    @ExceptionHandler(BlockException.class)
    public Result handleBlockException(BlockException e) {
        String message = "請求過于頻繁,請稍后再試";
        if (e instanceof FlowException) {
            message = "接口限流:" + message;
        } elseif (e instanceof DegradeException) {
            message = "服務降級:系統繁忙,請稍后再試";
        } elseif (e instanceof ParamFlowException) {
            message = "熱點參數限流:請求過于頻繁";
        } elseif (e instanceof SystemBlockException) {
            message = "系統保護:系統資源不足";
        } elseif (e instanceof AuthorityException) {
            message = "授權控制:沒有訪問權限";
        }
        
        return Result.error(429, message);
    }
}

在這個異常處理器中,我們根據不同的 BlockException 類型返回不同的錯誤信息,提示用戶請求被限流的原因。

4.6 使用 @SentinelResource 注解

為了讓 Sentinel 能夠對接口進行限流,我們需要在接口方法上使用 @SentinelResource 注解。以下是具體的代碼實現:

@RestController
@RequestMapping("/api")
publicclass UserController {
    
    // 使用資源名定義限流資源
    @SentinelResource(value = "getUserById", 
                      blockHandler = "getUserBlockHandler",
                      fallback = "getUserFallback")
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
    
    // 限流處理方法
    public User getUserBlockHandler(Long id, BlockException e) {
        log.warn("Get user request blocked: {}", id, e);
        thrownew RuntimeException("請求頻率過高,請稍后再試");
    }
    
    // 異常回退方法
    public User getUserFallback(Long id, Throwable t) {
        log.error("Get user failed: {}", id, t);
        User fallbackUser = new User();
        fallbackUser.setId(id);
        fallbackUser.setName("Unknown");
        return fallbackUser;
    }
}

在這個示例中,我們在 getUser 方法上使用了 @SentinelResource 注解,指定了資源名、限流處理方法和異常回退方法。當請求被限流時,會調用 getUserBlockHandler 方法進行處理。當方法執行過程中出現異常時,會調用 getUserFallback 方法進行回退。

4.7 更復雜的限流規則配置

除了基本的流控規則,Sentinel 還支持更復雜的限流規則配置,如基于 QPS + 調用關系的限流規則、基于并發線程數的限流規則、熱點參數限流規則等。以下是具體的代碼實現:

@Service
@Slf4j
publicclass SentinelRuleService {
    
    public void initComplexFlowRules() {
        List<FlowRule> rules = new ArrayList<>();
        
        // 基于 QPS + 調用關系的限流規則
        FlowRule apiRule = new FlowRule();
        apiRule.setResource("/api/data");
        apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        apiRule.setCount(20);
        
        // 限制調用來源
        apiRule.setLimitApp("frontend"); // 只限制來自前端應用的調用
        
        // 流控策略:關聯資源
        apiRule.setStrategy(RuleConstant.STRATEGY_RELATE);
        apiRule.setRefResource("/api/important"); // 當 important 接口 QPS 高時,限制 data 接口
        
        rules.add(apiRule);
        
        // 基于并發線程數的限流
        FlowRule threadRule = new FlowRule();
        threadRule.setResource("/api/heavy-task");
        threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 基于線程數
        threadRule.setCount(5); // 最多 5 個線程同時處理
        rules.add(threadRule);
        
        // 加載規則
        FlowRuleManager.loadRules(rules);
    }
    
    public void initHotspotRules() {
        // 熱點參數限流規則
        List<ParamFlowRule> rules = new ArrayList<>();
        
        ParamFlowRule rule = new ParamFlowRule("/api/product");
        // 對第 0 個參數(productId)進行限流
        rule.setParamIdx(0);
        rule.setCount(5);
        
        // 特例配置
        ParamFlowItem item1 = new ParamFlowItem();
        item1.setObject("1"); // productId = 1 的商品
        item1.setCount(10);  // 可以有更高的 QPS
        
        ParamFlowItem item2 = new ParamFlowItem();
        item2.setObject("2"); // productId = 2 的商品
        item2.setCount(2);   // 更嚴格的限制
        
        rule.setParamFlowItemList(Arrays.asList(item1, item2));
        
        rules.add(rule);
        ParamFlowRuleManager.loadRules(rules);
    }
}

在這個服務中,我們定義了基于 QPS + 調用關系的限流規則、基于并發線程數的限流規則和熱點參數限流規則,并使用 FlowRuleManager 和 ParamFlowRuleManager 加載這些規則。

優缺點分析

優點

  • 功能全面:Sentinel 支持 QPS 限流、并發線程數限流、熱點參數限流等多種限流方式,還提供了熔斷、系統保護等功能,可以滿足不同場景的需求。
  • 支持多種控制策略:可以選擇直接拒絕、預熱、排隊等多種控制策略,根據實際情況靈活調整限流行為。
  • 提供控制臺可視化管理:Sentinel 提供了可視化的控制臺,可以實時監控系統的流量情況,方便進行規則的配置和管理。
  • 支持動態規則調整:可以通過控制臺或 API 動態調整限流規則,無需重啟應用,提高了系統的靈活性和可維護性。
  • 可與 Spring Cloud 體系無縫集成:Sentinel 可以與 Spring Cloud 體系無縫集成,方便在微服務架構中使用。

缺點

  • 學習曲線較陡峭:Sentinel 的功能豐富,配置復雜,對于初學者來說,學習成本較高。
  • 分布式場景下需要額外配置規則持久化:在分布式場景下,需要額外配置規則持久化,確保不同實例之間的規則一致性。
  • 引入了額外的依賴:集成 Sentinel 需要引入額外的依賴,增加了項目的復雜度和維護成本。

5. 驗證碼與行為分析防刷

對于某些敏感操作,如登錄、注冊、支付等,單純的限流可能無法有效防止惡意請求。此時,可以結合驗證碼和行為分析來進一步增強系統的安全性,有效區分人類用戶和自動化腳本。

實現步驟

5.1 圖形驗證碼實現

首先,我們需要添加圖形驗證碼的依賴:

<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

5.2 創建驗證碼服務

接下來,我們需要創建一個驗證碼服務,用于生成和驗證驗證碼。以下是具體的代碼實現:

@Service
publicclass CaptchaService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    privatestaticfinallong CAPTCHA_EXPIRE_TIME = 5 * 60; // 5 分鐘
    
    /**
     * 生成驗證碼
     * @param request HTTP 請求
     * @param response HTTP 響應
     * @return 驗證碼 Base64 字符串
     */
    public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) {
        // 生成驗證碼
        SpecCaptcha captcha = new SpecCaptcha(130, 48, 5);
        
        // 生成驗證碼 ID
        String captchaId = UUID.randomUUID().toString();
        
        // 將驗證碼存入 Redis
        redisTemplate.opsForValue().set(
            "captcha:" + captchaId, 
            captcha.text().toLowerCase(), 
            CAPTCHA_EXPIRE_TIME, 
            TimeUnit.SECONDS
        );
        
        // 設置 Cookie
        Cookie cookie = new Cookie("captchaId", captchaId);
        cookie.setMaxAge((int) CAPTCHA_EXPIRE_TIME);
        cookie.setPath("/");
        response.addCookie(cookie);
        
        // 返回 Base64 編碼的驗證碼圖片
        return captcha.toBase64();
    }
    
    /**
     * 驗證驗證碼
     * @param request HTTP 請求
     * @param captchaCode 用戶輸入的驗證碼
     * @return 是否驗證通過
     */
    public boolean validateCaptcha(HttpServletRequest request, String captchaCode) {
        // 從 Cookie 獲取驗證碼 ID
        Cookie[] cookies = request.getCookies();
        String captchaId = null;
        
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("captchaId".equals(cookie.getName())) {
                    captchaId = cookie.getValue();
                    break;
                }
            }
        }
        
        if (captchaId == null) {
            returnfalse;
        }
        
        // 從 Redis 獲取正確的驗證碼
        String key = "captcha:" + captchaId;
        String correctCode = redisTemplate.opsForValue().get(key);
        
        // 驗證成功后刪除驗證碼
        if (correctCode != null && correctCode.equals(captchaCode.toLowerCase())) {
            redisTemplate.delete(key);
            returntrue;
        }
        
        returnfalse;
    }
}

在這個服務中,我們使用 easy-captcha 庫生成驗證碼,并將驗證碼存入 Redis 中。在驗證驗證碼時,我們從 Cookie 中獲取驗證碼 ID,然后從 Redis 中獲取正確的驗證碼進行比對。

5.3 創建驗證碼控制器

為了方便前端獲取驗證碼,我們需要創建一個驗證碼控制器。以下是具體的代碼實現:

@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {
    
    @Autowired
    private CaptchaService captchaService;
    
    @GetMapping
    public Map<String, String> getCaptcha(HttpServletRequest request, HttpServletResponse response) {
        String base64 = captchaService.generateCaptcha(request, response);
        return Map.of("captcha", base64);
    }
}

在這個控制器中,我們提供了一個 GET 請求接口,用于生成驗證碼并返回 Base64 編碼的驗證碼圖片。

5.4 創建驗證碼注解

為了標記需要進行驗證碼驗證的接口方法,我們可以創建一個驗證碼注解。以下是具體的代碼實現:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {
    String captchaParam() default "captchaCode";
}

在這個注解中,我們可以通過 captchaParam 屬性指定驗證碼參數的名稱。

5.5 實現驗證碼攔截器

接下來,我們需要實現一個驗證碼攔截器,在請求進入接口之前進行驗證碼驗證。以下是具體的代碼實現:

@Component
publicclass CaptchaInterceptor implements HandlerInterceptor {
    
    @Autowired
    private CaptchaService captchaService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            returntrue;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        CaptchaRequired captchaRequired = handlerMethod.getMethodAnnotation(CaptchaRequired.class);
        
        if (captchaRequired == null) {
            returntrue;
        }
        
        // 獲取驗證碼參數
        String captchaParam = captchaRequired.captchaParam();
        String captchaCode = request.getParameter(captchaParam);
        
        if (StringUtils.hasText(captchaCode)) {
            // 驗證驗證碼
            boolean valid = captchaService.validateCaptcha(request, captchaCode);
            if (valid) {
                returntrue;
            }
        }
        
        // 驗證失敗
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        response.getWriter().write("{\"code\":400,\"message\":\"驗證碼錯誤或已過期\"}");
        returnfalse;
    }
}

在這個攔截器中,我們首先判斷請求的處理方法是否標記了 @CaptchaRequired 注解。如果標記了,則獲取驗證碼參數并調用 CaptchaService 的 validateCaptcha 方法進行驗證。如果驗證失敗,則返回一個錯誤響應,提示用戶驗證碼錯誤或已過期。

5.6 創建行為分析服務

除了驗證碼,我們還可以通過行為分析來檢測可疑的機器行為。以下是具體的代碼實現:

@Service
@Slf4j
publicclass BehaviorAnalysisService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 檢查是否是可疑的機器行為
     * @param request HTTP 請求
     * @return 是否可疑
     */
    public boolean isSuspicious(HttpServletRequest request) {
        // 1. 獲取客戶端信息
        String ip = getIpAddress(request);
        String userAgent = request.getHeader("User-Agent");
        String requestId = request.getSession().getId();
        
        // 2. 檢查訪問頻率
        String freqKey = "behavior:freq:" + ip;
        Long count = redisTemplate.opsForValue().increment(freqKey, 1);
        redisTemplate.expire(freqKey, 1, TimeUnit.MINUTES);
        
        if (count != null && count > 30) {
            log.warn("訪問頻率異常: IP={}, count={}", ip, count);
            returntrue;
        }
        
        // 3. 檢查 User-Agent
        if (userAgent == null || isBotuserAgent(userAgent)) {
            log.warn("可疑的 User-Agent: {}", userAgent);
            returntrue;
        }
        
        // 4. 檢查請求時間模式
        String timeKey = "behavior:time:" + ip;
        long now = System.currentTimeMillis();
        String lastTimeStr = redisTemplate.opsForValue().get(timeKey);
        
        if (lastTimeStr != null) {
            long lastTime = Long.parseLong(lastTimeStr);
            long interval = now - lastTime;
            
            // 如果請求間隔非常均勻,可能是機器人
            if (isUniformInterval(ip, interval)) {
                log.warn("請求間隔異常均勻: IP={}, interval={}", ip, interval);
                returntrue;
            }
        }
        
        redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);
        
        // 更多高級檢測邏輯...
        
        returnfalse;
    }
    
    /**
     * 檢查是否是機器人 UA
     */
    private boolean isBotuserAgent(String userAgent) {
        String ua = userAgent.toLowerCase();
        return ua.contains("bot") || ua.contains("spider") || ua.contains("crawl") ||
               ua.isEmpty() || ua.length() < 40;
    }
    
    /**
     * 檢查請求間隔是否異常均勻
     */
    private boolean isUniformInterval(String ip, long interval) {
        String key = "behavior:intervals:" + ip;
        
        // 獲取最近的幾個間隔
        List<String> intervalStrs = redisTemplate.opsForList().range(key, 0, 4);
        redisTemplate.opsForList().leftPush(key, String.valueOf(interval));
        redisTemplate.opsForList().trim(key, 0, 9);  // 只保留最近 10 個
        redisTemplate.expire(key, 10, TimeUnit.MINUTES);
        
        if (intervalStrs == null || intervalStrs.size() < 5) {
            returnfalse;
        }
        
        // 計算間隔的方差,方差小說明請求間隔很均勻
        List<Long> intervals = intervalStrs.stream()
                .map(Long::parseLong)
                .collect(Collectors.toList());
        
        double mean = intervals.stream().mapToLong(Long::longValue).average().orElse(0);
        double variance = intervals.stream()
                .mapToDouble(i -> Math.pow(i - mean, 2))
                .average()
                .orElse(0);
        
        return variance < 100;  // 方差閾值,需要根據實際情況調整
    }
    
    // getIpAddress 方法同上
}

在這個服務中,我們從多個維度對請求進行分析,包括訪問頻率、User-Agent 和請求時間模式。如果發現可疑的行為,我們會記錄日志并返回 true

5.7 創建行為分析攔截器

最后,我們需要創建一個行為分析攔截器,在請求進入接口之前進行行為分析。以下是具體的代碼實現:

@Component
publicclass BehaviorAnalysisInterceptor implements HandlerInterceptor {
    
    @Autowired
    private BehaviorAnalysisService behaviorAnalysisService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 對于需要保護的端點進行檢查
        String path = request.getRequestURI();
        if (path.startsWith("/api/") && isPotentialRiskEndpoint(path)) {
            boolean suspicious = behaviorAnalysisService.isSuspicious(request);
            
            if (suspicious) {
                // 需要驗證碼或其他額外驗證
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                response.getWriter().write("{\"code\":429,\"message\":\"檢測到異常訪問,請進行驗證\",\"needCaptcha\":true}");
                returnfalse;
            }
        }
        
        returntrue;
    }
    
    /**
     * 判斷是否是高風險端點
     */
    private boolean isPotentialRiskEndpoint(String path) {
        return path.contains("/login") || 
               path.contains("/register") || 
               path.contains("/payment") || 
               path.contains("/order") ||
               path.contains("/password");
    }
}

在這個攔截器中,我們首先判斷請求的 URI 是否以 /api/ 開頭,并且是否是高風險端點。如果是,則調用 BehaviorAnalysisService 的 isSuspicious 方法進行行為分析。如果發現可疑行為,則返回一個錯誤響應,提示用戶進行驗證。

5.8 使用示例

以下是一個使用驗證碼和行為分析的示例:

@RestController
@RequestMapping("/api")
publicclass UserController {
    
    @CaptchaRequired
    @PostMapping("/login")
    public Result login(@RequestParam String username, 
                        @RequestParam String password,
                        @RequestParam String captchaCode) {
        // 登錄邏輯
        return userService.login(username, password);
    }
    
    @CaptchaRequired
    @PostMapping("/register")
    public Result register(@RequestBody UserRegisterDTO registerDTO,
                          @RequestParam String captchaCode) {
        // 注冊邏輯
        return userService.register(registerDTO);
    }
}

在這個示例中,我們在 login 和 register 方法上使用了 @CaptchaRequired 注解,要求用戶輸入驗證碼進行驗證。

優缺點分析

優點

  • 能有效區分人類用戶和自動化腳本:通過驗證碼和行為分析,可以準確地識別出自動化腳本的惡意請求,有效防止刷接口行為。
  • 對惡意用戶有較強的阻止作用:驗證碼和行為分析的雙重防護,大大增加了惡意用戶的攻擊成本,使其難以得逞。
  • 針對敏感操作提供額外安全層:對于登錄、注冊、支付等敏感操作,驗證碼和行為分析可以提供額外的安全保障,保護用戶的隱私和財產安全。
  • 可以實現自適應安全策略:根據用戶的行為特征和系統的安全狀況,可以動態調整驗證碼和行為分析的策略,實現自適應的安全防護。

缺點

  • 增加了用戶操作成本,可能影響用戶體驗:要求用戶輸入驗證碼會增加用戶的操作步驟,降低用戶體驗。特別是對于一些頻繁操作的用戶,可能會感到厭煩。
  • 實現復雜,需要前后端配合:驗證碼和行為分析的實現涉及到前后端的多個環節,需要前后端密切配合,增加了開發和維護的難度。
  • 某些驗證碼可能被 OCR 技術破解:雖然現在的驗證碼技術不斷發展,但仍然存在被 OCR 技術破解的風險,需要不斷更新和改進驗證碼的生成和驗證方式。
  • 行為分析可能產生誤判:行為分析是基于一定的規則和算法進行的,可能會出現誤判的情況,將正常用戶的行為誤判為可疑行為,影響用戶的正常使用。

方案對比與選擇

方案

實現難度

防刷效果

分布式支持

用戶體驗

適用場景

基于注解的訪問頻率限制

需配合 Redis

一般

一般接口,簡單場景

令牌桶算法

中高

單機

允許突發流量的場景

分布式限流(Redis+Lua)

支持

一般

分布式系統,精確限流

Sentinel

中高

需額外配置

可配置

復雜系統,多維度防護

驗證碼與行為分析

支持

較差

敏感操作,關鍵業務

總結

接口防刷是一個系統性工程,需要綜合考慮安全性、用戶體驗、性能開銷和運維復雜度等多方面因素。本文介紹的 5 種方案各有優缺點,你可以根據實際需求靈活選擇和組合。

無論采用哪種方案,接口防刷都應該遵循以下原則:

  • 最小影響原則:盡量不影響正常用戶的體驗,確保系統在安全的前提下能夠高效運行。
  • 梯度防護原則:根據接口的重要程度和風險等級,采用不同強度的防護措施,實現精準防護。
  • 可監控原則:提供充分的監控和告警機制,及時發現和處理異常情況,確保系統的安全性和穩定性。
  • 靈活調整原則:支持動態調整防護參數和策略,根據系統的運行情況和安全需求,及時做出調整。

通過合理實施接口防刷策略,可以有效提高系統的安全性和穩定性,為用戶提供更好的服務體驗。希望本文能對你有所幫助,讓你的系統在復雜的網絡環境中更加安全可靠!


責任編輯:武曉燕 來源: 碼猿技術專欄
相關推薦

2025-04-14 04:01:00

2025-01-22 14:02:35

2023-10-17 08:55:08

數據庫數據業務

2025-05-14 01:00:00

Spring工具工廠類

2022-05-30 16:31:08

CSS

2025-02-08 08:00:00

JavaDeepSeekIDEA

2024-08-29 15:26:21

2025-01-13 13:47:13

2021-03-04 09:31:42

開源技術 項目

2025-04-10 00:25:00

Spring@JsonView注解

2023-12-10 20:33:50

Redis搜索全文

2025-06-06 08:28:56

2025-06-12 08:21:22

2025-06-26 01:22:00

SpringBean開發

2024-01-30 09:21:29

CSS文字效果文字裝飾

2022-06-08 08:01:28

模板字面量類型

2021-08-05 16:25:37

Windows 11Windows微軟

2024-06-07 09:06:36

2025-01-20 10:22:23

2024-09-13 10:21:50

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产欧美精品一区二区 | www.黄色在线观看 | 一级全黄少妇性色生活免费看 | 91国产在线播放 | 亚洲国产精品suv | 欧美a免费 | 亚洲国产精品久久久久婷婷老年 | 在线精品一区二区 | 欧美一区二区三区大片 | 精品一区二区av | 在线观看免费av网 | 久久精品国产一区二区电影 | 久久成人国产 | 亚洲视频在线看 | 国产精品黄视频 | 国产视频中文字幕在线观看 | 亚洲高清av| 免费亚洲婷婷 | av免费网站在线观看 | 精精国产xxxx视频在线播放 | 日韩成人高清在线 | 99re在线视频免费观看 | 久久精品日 | 国产69久久精品成人看动漫 | 婷婷综合久久 | 二区三区在线观看 | 国产一区二区在线视频 | 日韩综合 | 国产精品久久久久久高潮 | 另类 综合 日韩 欧美 亚洲 | 国产精品免费一区二区三区 | 国产乱码精品一品二品 | av入口| 成人亚洲精品久久久久软件 | 久久综合色综合 | 精品久久久久久久 | 伊人久久在线 | 欧美1区2区 | 亚洲国产91 | 亚洲乱码一区二区三区在线观看 | 日韩在线免费视频 |