SpringBoot 與 Sentinel 整合,解決 DDoS 攻擊與異常爬蟲請求問題
Sentinel 是阿里巴巴開源的一款面向分布式服務架構的輕量級高可用流量控制組件,主要用于流量控制、熔斷降級和系統負載保護。 雖然 Sentinel 主要用于微服務場景下的流量管理和故障隔離,但也可以通過一些策略和配置來輔助防御 DDoS 攻擊和異常爬蟲請求。
DDoS攻擊
DDoS(Distributed Denial of Service)是一種惡意攻擊手段,攻擊者通過控制大量計算機設備(如僵尸網絡),向目標服務器發送大量的數據包或請求,從而耗盡服務器的帶寬、CPU資源或其他系統資源,導致合法用戶無法正常訪問服務。
(1) 常見類型:
① Volume-based Attacks (體積型攻擊):
- 例如ICMP Flood、UDP Flood。
- 攻擊者發送大量無用的數據包,占用帶寬。
② Protocol Attacks (協議型攻擊):
- 例如SYN Flood、ACK Flood。
- 攻擊者利用TCP/IP協議漏洞,發送特定的數據包使服務器崩潰。
③ Application Layer Attacks (應用層攻擊):
- 例如HTTP Flood、Slowloris。
- 攻擊者模擬真實用戶的行為,發送大量的HTTP請求,消耗服務器的應用層資源。
(2) 防御措施:
- 使用CDN: 內容分發網絡可以幫助分散流量,減輕單個服務器的壓力。
- 負載均衡: 分散請求到多個服務器上,提高系統的可用性。
- 防火墻和入侵檢測系統: 防止非法流量進入服務器。
- Rate Limiting (限流): 控制每個IP地址或來源的請求速率,防止過載。
- Traffic Shaping (流量整形): 調整進出網絡的數據包傳輸速率,優化流量分配。
- Anycast IP Addressing: 使用多條路徑將流量引導至最近的健康節點,提高冗余性和抗攻擊能力。
異常爬蟲請求
異常爬蟲是指那些不符合正常爬蟲行為規范的自動化程序,它們可能會對網站造成負擔,甚至破壞網站的正常運行。這些爬蟲可能用于抓取敏感信息、進行競爭情報收集、參與SEO欺詐等活動。
(1) 特點:
- 高頻率請求: 在短時間內發送大量請求,可能導致服務器過載。
- 不遵循robots.txt: 忽略網站的爬蟲協議文件,訪問受保護的內容。
- 偽裝成普通用戶: 使用偽造的User-Agent字符串,難以識別。
- 頻繁更改IP: 使用代理或VPN頻繁更換IP地址,增加追蹤難度。
(2) 防御措施:
- 設置Robots.txt: 明確告知爬蟲哪些內容可以抓取,哪些不可以。
- Rate Limiting (限流): 限制每個IP地址或來源的請求速率,防止濫用。
- CAPTCHA (驗證碼): 在關鍵操作前要求用戶提供驗證碼,區分人機。
- IP黑名單/白名單: 阻止已知惡意IP地址的訪問,允許信任的IP地址。
- User-Agent過濾: 檢查請求的User-Agent字段,阻止非標準的爬蟲請求。
- Session Management: 使用會話管理技術,識別和限制可疑的爬蟲行為。
- Dynamic Content Delivery: 動態生成內容,使得爬蟲難以抓取有用的信息。
- Monitoring and Logging: 實時監控和記錄異常請求,及時發現和響應潛在威脅。
實現思路
(1) 流控(Flow Control):
- 流控用于限制某個資源的訪問速率,防止系統過載。
- 通過設置每秒允許的最大請求數,當超過這個閾值時,Sentinel會阻止多余的請求,并返回相應的錯誤信息。
(2) 降級(Degrade):
- 降級用于在系統壓力過大時自動降低服務的可用性,保護核心業務不受影響。
- 可以根據不同的策略(如RT、異常比例、異常數)來進行降級處理。
(3) 熱點參數限流(Hotspot Parameter Flow Control):
熱點參數限流用于針對特定參數進行限流,防止某些參數導致的服務過載。
(4) 全局異常處理器:
捕獲并處理由Sentinel拋出的異常,返回友好的錯誤信息給客戶端。
(55) 自定義異常處理器:
根據不同的異常類型(如FlowException和DegradeException),返回具體的錯誤信息。
先啟動Nacos服務器
我已經在本地啟動了Nacos服務器。
你也可以從Nacos GitHub https://github.com/alibaba/nacos 下載并按照說明啟動。
上傳Sentinel規則到Nacos
在Nacos配置管理中創建兩個配置文件:
- Data ID: sentinel-demo-flow-rules, Group: DEFAULT_GROUP
[
{
"resource": "/api/hello",
"limitApp": "default",
"grade": 1,
"count": 10,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- Data ID: sentinel-demo-degrade-rules, Group: DEFAULT_GROUP
[]
代碼實操
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/><!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sentinel-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sentinel-demo</name>
<description>Demo project for Spring Boot with Sentinel</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Alibaba Sentinel Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Lombok for reducing boilerplate code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port:8080
spring:
cloud:
sentinel:
transport:
dashboard:localhost:8080# 配置Sentinel控制臺地址
datasource:
ds1:
nacos:
server-addr:localhost:8848# Nacos服務器地址
data-id:${spring.application.name}-flow-rules# 流控規則數據ID
group:DEFAULT_GROUP# 流控規則組名
rule-type:flow# 規則類型為流控規則
logging:
level:
root:INFO# 設置根日志級別為INFO
com.example.sentineldemo:DEBUG# 設置應用包的日志級別為DEBUG
logback-spring.xml
<!-- Logback日志配置文件 -->
<configuration>
<!-- 定義控制臺輸出器 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern><!-- 日志格式 -->
</encoder>
</appender>
<!-- 根日志記錄器配置 -->
<root level="info">
<appender-ref ref="STDOUT"/><!-- 將日志輸出到控制臺 -->
</root>
</configuration>
flow-rules.json
放在 src/main/resources/sentinel/
[
{
"resource": "/api/hello", // 資源路徑
"limitApp": "default", // 默認限流應用
"grade": 1, // QPS模式
"count": 10, // 每秒最大請求數
"strategy": 0, // 直接模式
"controlBehavior": 0, // 快速失敗策略
"clusterMode": false // 非集群模式
}
]
定義了一個流控規則,限制 /api/hello 接口每秒最多允許 10 個請求。
如果超過這個閾值,Sentinel 會阻止多余的請求,并返回 "Too many requests, please try again later."。
SentinelDemoApplication.java
package com.example.sentineldemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot應用主類
*/
@SpringBootApplication
public class SentinelDemoApplication {
/**
* 應用程序入口點
*
* @param args 命令行參數
*/
public static void main(String[] args) {
SpringApplication.run(SentinelDemoApplication.class, args);
}
}
Sentinel配置類
package com.example.sentineldemo.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* Sentinel配置類
*/
@Configuration
publicclass SentinelConfig {
/**
* 初始化Sentinel規則
*/
@PostConstruct
private void initRules() {
String serverAddr = "localhost"; // Nacos服務器地址
String groupId = "DEFAULT_GROUP"; // 規則組名
String dataId = "${spring.application.name}-flow-rules"; // 流控規則數據ID
// 從Nacos讀取流控規則
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(serverAddr, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
String degradeDataId = "${spring.application.name}-degrade-rules"; // 降級規則數據ID
// 從Nacos讀取降級規則
ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new NacosDataSource<>(serverAddr, groupId, degradeDataId,
source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {}));
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
}
/**
* 自定義請求來源解析器
*
* @return RequestOriginParser實例
*/
@Bean
public RequestOriginParser requestOriginParser() {
return request -> request.getHeader("origin"); // 使用HTTP頭中的origin字段作為請求來源
}
}
Controller
使用 @SentinelResource 注解來標識需要保護的方法。
當方法被調用時,Sentinel 會根據預先定義的規則進行檢查。
package com.example.sentineldemo.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.example.sentineldemo.exception.BlockExceptionHandler;
import com.example.sentineldemo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 控制器類,處理API請求
*/
@RestController
publicclass HelloController {
@Autowired
private HelloService helloService; // 注入服務層對象
/**
* 處理GET /api/hello請求
*
* @param name 請求參數,用戶名
* @return 返回問候語
*/
@GetMapping("/api/hello")
@SentinelResource(value = "hello", blockHandlerClass = BlockExceptionHandler.class, blockHandler = "handleException")
public String sayHello(@RequestParam(required = false) String name) {
if (name == null || name.isEmpty()) {
name = "World"; // 如果未提供名字,默認為"World"
}
return helloService.getGreeting(name); // 調用服務層獲取問候語
}
}
全局異常處理器
捕獲并處理由 Sentinel 拋出的 BlockException 異常。
返回友好的錯誤信息給客戶端。
package com.example.sentineldemo.exception;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* 全局異常處理器
*/
@ControllerAdvice
publicclass GlobalExceptionHandler {
/**
* 處理Sentinel阻塞異常
*
* @param ex 異常對象
* @return 返回錯誤信息和狀態碼
*/
@ExceptionHandler(BlockException.class)
public ResponseEntity<String> handleBlockException(BlockException ex) {
returnnew ResponseEntity<>("Blocked by Sentinel: " + ex.getClass().getSimpleName(), HttpStatus.TOO_MANY_REQUESTS);
}
}
Sentinel資源塊處理異常處理器
package com.example.sentineldemo.exception;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
/**
* Sentinel資源塊處理異常處理器
*/
publicclass BlockExceptionHandler {
/**
* 處理Sentinel資源塊異常
*
* @param ex 異常對象
* @return 返回錯誤信息
*/
public static String handleException(BlockException ex) {
if (ex instanceof FlowException) {
return"Too many requests, please try again later."; // 流控異常處理
} elseif (ex instanceof DegradeException) {
return"Service is degraded, please try again later."; // 降級異常處理
}
return"Request blocked by Sentinel."; // 其他異常處理
}
}
服務層
package com.example.sentineldemo.service;
import org.springframework.stereotype.Service;
/**
* 服務層類,處理業務邏輯
*/
@Service
public class HelloService {
/**
* 獲取問候語
*
* @param name 用戶名
* @return 返回問候語
*/
public String getGreeting(String name) {
return "Hello, " + name + "!"; // 構造問候語
}
}