SpringBoot 接口防刷的五種方案,太強(qiáng)了!
在當(dāng)今的互聯(lián)網(wǎng)環(huán)境中,接口防刷已成為保障系統(tǒng)安全與穩(wěn)定運(yùn)行的關(guān)鍵環(huán)節(jié)。惡意的高頻請(qǐng)求如同隱藏在暗處的“殺手”,不僅會(huì)大量消耗服務(wù)器寶貴的資源,還可能引發(fā)數(shù)據(jù)異常,嚴(yán)重時(shí)甚至?xí)?dǎo)致整個(gè)系統(tǒng)癱瘓,給業(yè)務(wù)帶來不可估量的損失。
本文將深入剖析在 Spring Boot 框架下實(shí)現(xiàn)接口防刷的 5 種技術(shù)方案,幫助你根據(jù)實(shí)際需求選擇最合適的方法,為系統(tǒng)安全保駕護(hù)航。
1. 基于注解的訪問頻率限制
基于注解的訪問頻率限制是一種廣受歡迎的接口防刷方案,它通過自定義注解和 AOP 切面的巧妙結(jié)合,實(shí)現(xiàn)了對(duì)接口訪問頻率的有效控制。這種方法簡(jiǎn)單易用,實(shí)現(xiàn)成本低,就像給接口穿上了一層輕便的“防護(hù)衣”。
實(shí)現(xiàn)步驟
1.1 創(chuàng)建限流注解
首先,我們需要?jiǎng)?chuàng)建一個(gè)自定義的限流注解,用于標(biāo)記需要進(jìn)行訪問頻率限制的接口方法。以下是具體的代碼實(shí)現(xiàn):
圖片
在這個(gè)注解中,我們可以通過 time 屬性設(shè)置限制的時(shí)間段,通過 count 屬性設(shè)置在該時(shí)間段內(nèi)允許的最大請(qǐng)求次數(shù),通過 key 屬性指定限流的鍵,支持使用 SpEL 表達(dá)式進(jìn)行靈活配置,message 屬性則用于在請(qǐng)求被限定時(shí)給用戶提供提示信息。
1.2 實(shí)現(xiàn)限流切面
接下來,我們需要實(shí)現(xiàn)一個(gè) AOP 切面,用于處理限流邏輯。以下是具體的代碼實(shí)現(xiàn):
@Aspect
@Component
@Slf4j
publicclass RateLimitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
// 獲取請(qǐng)求的方法名
String methodName = pjp.getSignature().getName();
// 獲取請(qǐng)求的類名
String className = pjp.getTarget().getClass().getName();
// 組合限流 key
String limitKey = getLimitKey(pjp, rateLimit, methodName, className);
// 獲取限流參數(shù)
int time = rateLimit.time();
int count = rateLimit.count();
// 執(zhí)行限流邏輯
boolean limited = isLimited(limitKey, time, count);
if (limited) {
thrownew RuntimeException(rateLimit.message());
}
// 執(zhí)行目標(biāo)方法
return pjp.proceed();
}
private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {
// 獲取用戶自定義的 key
String key = rateLimit.key();
if (StringUtils.hasText(key)) {
// 支持 SpEL 表達(dá)式解析
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 {
// 默認(rèn)使用類名+方法名+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 的計(jì)數(shù)器實(shí)現(xiàn)限流
try {
Long currentCount = redisTemplate.opsForValue().increment(key, 1);
// 如果是第一次訪問,設(shè)置過期時(shí)間
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;
}
}在這個(gè)切面中,我們使用 @Around 注解攔截所有標(biāo)記了 @RateLimit 注解的方法。在方法執(zhí)行前,我們會(huì)根據(jù)注解中的參數(shù)計(jì)算出限流的鍵,并使用 Redis 的計(jì)數(shù)器來記錄請(qǐng)求次數(shù)。如果請(qǐng)求次數(shù)超過了限制,我們會(huì)拋出一個(gè)異常,提示用戶操作過于頻繁。
1.3 使用示例
以下是一個(gè)使用 @RateLimit 注解的示例:
圖片
在這個(gè)示例中,我們?cè)?nbsp;getUser 方法上使用了 @RateLimit 注解,設(shè)置了 60 秒內(nèi)最多允許 3 次請(qǐng)求。在 updateUser 方法上,我們使用了 SpEL 表達(dá)式來動(dòng)態(tài)生成限流的鍵,實(shí)現(xiàn)了更加靈活的限流策略。
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 實(shí)現(xiàn)簡(jiǎn)單,上手容易:該方案的實(shí)現(xiàn)邏輯清晰,代碼量較少,即使是初學(xué)者也能快速掌握。在單機(jī)情況下,甚至可以去掉 Redis 換成本地緩存實(shí)現(xiàn),進(jìn)一步降低了實(shí)現(xiàn)難度。
- 注解式使用,對(duì)業(yè)務(wù)代碼無侵入:通過自定義注解的方式,我們可以將限流邏輯與業(yè)務(wù)邏輯分離,對(duì)業(yè)務(wù)代碼的改動(dòng)非常小,不會(huì)影響原有的業(yè)務(wù)功能。
- 可以精確控制接口粒度:我們可以針對(duì)不同的接口方法設(shè)置不同的限流參數(shù),實(shí)現(xiàn)對(duì)接口訪問頻率的精確控制。
- 支持靈活的限流策略配置:通過 SpEL 表達(dá)式,我們可以根據(jù)請(qǐng)求的參數(shù)、用戶信息等動(dòng)態(tài)生成限流的鍵,實(shí)現(xiàn)更加靈活的限流策略。
缺點(diǎn)
- 限流邏輯相對(duì)簡(jiǎn)單,無法應(yīng)對(duì)復(fù)雜場(chǎng)景:該方案的限流邏輯主要基于固定的時(shí)間窗口和請(qǐng)求次數(shù),對(duì)于一些復(fù)雜的場(chǎng)景,如突發(fā)流量、動(dòng)態(tài)限流等,可能無法滿足需求。
- 缺少預(yù)警機(jī)制:當(dāng)請(qǐng)求次數(shù)接近或達(dá)到限制時(shí),系統(tǒng)無法及時(shí)發(fā)出預(yù)警,可能會(huì)導(dǎo)致用戶體驗(yàn)下降。
2. 令牌桶算法實(shí)現(xiàn)限流
令牌桶算法是一種更加靈活的限流算法,它就像一個(gè)裝有令牌的桶,系統(tǒng)會(huì)以固定的速率向桶中添加令牌,每個(gè)請(qǐng)求需要從桶中獲取一個(gè)令牌才能被處理。這種算法可以允許突發(fā)流量,同時(shí)又能限制長(zhǎng)期的平均流量,為系統(tǒng)提供了更加靈活的流量控制能力。
實(shí)現(xiàn)步驟
2.1 引入依賴
Google 提供的 Guava 庫中包含了令牌桶的實(shí)現(xiàn),我們可以通過以下依賴將其引入項(xiàng)目:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>2.2 創(chuàng)建令牌桶限流器
接下來,我們需要?jiǎng)?chuàng)建一個(gè)令牌桶限流器,用于管理不同接口的令牌桶。以下是具體的代碼實(shí)現(xiàn):
@Component
publicclass RateLimiter {
// 使用 ConcurrentHashMap 存儲(chǔ)不同接口的令牌桶
privatefinal ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
/**
* 獲取特定接口的令牌桶,不存在則創(chuàng)建
* @param key 限流鍵
* @param permitsPerSecond 每秒允許的請(qǐng)求量
* @return 令牌桶實(shí)例
*/
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 每秒允許的請(qǐng)求量
* @param timeout 超時(shí)時(shí)間
* @param unit 時(shí)間單位
* @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);
}
}在這個(gè)限流器中,我們使用 ConcurrentHashMap 來存儲(chǔ)不同接口的令牌桶,確保線程安全。通過 getRateLimiter 方法,我們可以根據(jù)限流鍵獲取對(duì)應(yīng)的令牌桶,如果令牌桶不存在,則會(huì)自動(dòng)創(chuàng)建一個(gè)新的令牌桶。通過 tryAcquire 方法,我們可以嘗試從令牌桶中獲取一個(gè)令牌,如果在指定的超時(shí)時(shí)間內(nèi)獲取成功,則返回 true,否則返回 false。
2.3 創(chuàng)建攔截器
為了實(shí)現(xiàn)對(duì)接口的限流,我們需要?jiǎng)?chuàng)建一個(gè)攔截器,在請(qǐng)求進(jìn)入接口之前進(jìn)行令牌的獲取操作。以下是具體的代碼實(shí)現(xiàn):
圖片
在這個(gè)攔截器中,我們首先判斷請(qǐng)求的 URI 是否以 /api/ 開頭,如果是,則進(jìn)行限流處理。然后,我們獲取請(qǐng)求的 IP 地址和 URI,組合成限流鍵。接著,我們嘗試從令牌桶中獲取一個(gè)令牌,如果獲取失敗,則返回一個(gè)限流響應(yīng),提示用戶請(qǐng)求過于頻繁。
2.4 配置攔截器
最后,我們需要將攔截器配置到 Spring Boot 應(yīng)用中,使其生效。以下是具體的代碼實(shí)現(xiàn):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenBucketInterceptor tokenBucketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenBucketInterceptor)
.addPathPatterns("/**");
}
}在這個(gè)配置類中,我們使用 WebMvcConfigurer 接口的 addInterceptors 方法將攔截器添加到攔截器鏈中,并設(shè)置攔截所有的請(qǐng)求。
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 支持突發(fā)流量,不會(huì)完全拒絕短時(shí)高峰:令牌桶算法允許在短時(shí)間內(nèi)有較高的請(qǐng)求速率,只要桶中有足夠的令牌。當(dāng)突發(fā)流量到來時(shí),系統(tǒng)可以快速處理這些請(qǐng)求,而不會(huì)像固定窗口算法那樣直接拒絕請(qǐng)求。
- 平滑的限流效果,用戶體驗(yàn)更好:由于令牌桶算法是以固定的速率向桶中添加令牌,因此可以實(shí)現(xiàn)平滑的限流效果,避免了請(qǐng)求的突然中斷,提高了用戶體驗(yàn)。
- 可以配置不同接口的不同限流策略:通過使用不同的限流鍵,我們可以為不同的接口配置不同的令牌桶,實(shí)現(xiàn)對(duì)不同接口的差異化限流。
- 無需額外的存儲(chǔ)設(shè)施:令牌桶算法的實(shí)現(xiàn)不需要額外的存儲(chǔ)設(shè)施,只需要在內(nèi)存中維護(hù)一個(gè)令牌桶即可,降低了系統(tǒng)的復(fù)雜度和成本。
缺點(diǎn)
- 只適用于單機(jī)部署,分布式環(huán)境需要額外改造:該方案的令牌桶是在內(nèi)存中維護(hù)的,因此只適用于單機(jī)部署的環(huán)境。在分布式環(huán)境中,需要將令牌桶的狀態(tài)存儲(chǔ)到共享的存儲(chǔ)設(shè)施中,如 Redis,才能實(shí)現(xiàn)分布式限流。
- 重啟應(yīng)用后狀態(tài)丟失:由于令牌桶的狀態(tài)是在內(nèi)存中維護(hù)的,因此當(dāng)應(yīng)用重啟后,令牌桶的狀態(tài)會(huì)丟失,需要重新初始化。
- 無法精確控制時(shí)間窗口內(nèi)的請(qǐng)求總量:令牌桶算法只能控制請(qǐng)求的平均速率,無法精確控制在某個(gè)時(shí)間窗口內(nèi)的請(qǐng)求總量。在某些對(duì)請(qǐng)求總量有嚴(yán)格限制的場(chǎng)景下,可能無法滿足需求。
3. 分布式限流(Redis + Lua 腳本)
在分布式系統(tǒng)中,單機(jī)限流方案往往難以滿足需求,因?yàn)椴煌膶?shí)例之間無法共享限流狀態(tài)。利用 Redis 和 Lua 腳本可以實(shí)現(xiàn)高效的分布式限流,確保系統(tǒng)在分布式環(huán)境下的安全性和穩(wěn)定性。
實(shí)現(xiàn)步驟
3.1 定義 Lua 腳本
首先,我們需要定義一個(gè) Redis 限流的 Lua 腳本,用于實(shí)現(xiàn)限流邏輯。以下是具體的腳本內(nèi)容:
圖片
在這個(gè)腳本中,我們首先獲取限流鍵、限流窗口、限流閾值和當(dāng)前時(shí)間戳。然后,我們移除過期的請(qǐng)求記錄,統(tǒng)計(jì)當(dāng)前窗口內(nèi)的請(qǐng)求數(shù)。如果請(qǐng)求數(shù)超過了閾值,我們返回 0 表示拒絕請(qǐng)求。否則,我們添加當(dāng)前請(qǐng)求記錄,并設(shè)置過期時(shí)間,最后返回當(dāng)前窗口剩余可用請(qǐng)求數(shù)。
3.2 創(chuàng)建 Redis 限流服務(wù)
接下來,我們需要?jiǎng)?chuàng)建一個(gè) Redis 限流服務(wù),用于執(zhí)行 Lua 腳本并處理限流邏輯。以下是具體的代碼實(shí)現(xiàn):
@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);
}
/**
* 嘗試獲取訪問權(quán)限
* @param key 限流鍵
* @param window 時(shí)間窗口(秒)
* @param threshold 閾值
* @return 剩余可用請(qǐng)求數(shù),-1 表示被限流
*/
public long isAllowed(String key, int window, int threshold) {
try {
// 執(zhí)行 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);
// 發(fā)生異常時(shí)放行請(qǐng)求
return threshold;
}
}
}在這個(gè)服務(wù)中,我們使用 StringRedisTemplate 來執(zhí)行 Lua 腳本。在 init 方法中,我們加載 Lua 腳本并設(shè)置返回值類型。在 isAllowed 方法中,我們執(zhí)行 Lua 腳本并根據(jù)返回值判斷是否允許訪問。如果返回值為 -1 表示被限流,否則返回剩余可用請(qǐng)求數(shù)。
3.3 創(chuàng)建分布式限流注解
為了方便使用,我們可以創(chuàng)建一個(gè)分布式限流注解,用于標(biāo)記需要進(jìn)行分布式限流的接口方法。以下是具體的代碼實(shí)現(xiàn):
圖片
在這個(gè)注解中,我們可以通過 prefix 屬性設(shè)置限流鍵的前綴,通過 window 屬性設(shè)置時(shí)間窗口,通過 threshold 屬性設(shè)置時(shí)間窗口內(nèi)允許的最大請(qǐng)求數(shù),通過 mode 屬性設(shè)置限流模式。
3.4 實(shí)現(xiàn)分布式限流切面
接下來,我們需要實(shí)現(xiàn)一個(gè) AOP 切面,用于處理分布式限流邏輯。以下是具體的代碼實(shí)現(xiàn):
@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("接口訪問過于頻繁,請(qǐng)稍后再試");
}
// 執(zhí)行目標(biāo)方法
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);
// 根據(jù)限流模式添加不同的后綴
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 的代碼同上一個(gè)示例
return"127.0.0.1"; // 簡(jiǎn)化處理
}
// 獲取當(dāng)前用戶 ID,根據(jù)實(shí)際認(rèn)證系統(tǒng)實(shí)現(xiàn)
private Object getUserId() {
// 這里簡(jiǎn)化處理,實(shí)際中應(yīng)從認(rèn)證信息中獲取
// 例如:SecurityContextHolder.getContext().getAuthentication().getPrincipal()
returnnull;
}
}在這個(gè)切面中,我們使用 @Around 注解攔截所有標(biāo)記了 @DistributedRateLimit 注解的方法。在方法執(zhí)行前,我們會(huì)根據(jù)注解中的參數(shù)生成限流鍵,并調(diào)用 RedisRateLimiterService 的 isAllowed 方法判斷是否允許訪問。如果不允許訪問,我們會(huì)拋出一個(gè)異常,提示用戶接口訪問過于頻繁。
3.5 使用示例
以下是一個(gè)使用 @DistributedRateLimit 注解的示例:
@RestController
@RequestMapping("/api")
publicclass PaymentController {
@DistributedRateLimit(prefix = "pay:", window = 3600, threshold = 5, mode = "user")
@PostMapping("/payment")
public Result createPayment(@RequestBody PaymentRequest paymentRequest) {
// 創(chuàng)建支付業(yè)務(wù)邏輯
return paymentService.createPayment(paymentRequest);
}
@DistributedRateLimit(window = 60, threshold = 30, mode = "ip")
@GetMapping("/products")
public List<Product> getProducts() {
// 查詢產(chǎn)品列表
return productService.findAll();
}
@DistributedRateLimit(window = 1, threshold = 100, mode = "all")
@GetMapping("/hot/resource")
public Resource getHotResource() {
// 獲取熱門資源
return resourceService.getHotResource();
}
}在這個(gè)示例中,我們?cè)?nbsp;createPayment 方法上使用了 @DistributedRateLimit 注解,設(shè)置了 3600 秒內(nèi)每個(gè)用戶最多允許 5 次請(qǐng)求。在 getProducts 方法上,我們?cè)O(shè)置了 60 秒內(nèi)每個(gè) IP 最多允許 30 次請(qǐng)求。在 getHotResource 方法上,我們?cè)O(shè)置了 1 秒內(nèi)整個(gè)接口最多允許 100 次請(qǐng)求。
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 適用于分布式系統(tǒng),多實(shí)例間共享限流狀態(tài):通過使用 Redis 作為共享存儲(chǔ),不同的實(shí)例可以共享限流狀態(tài),確保系統(tǒng)在分布式環(huán)境下的一致性和穩(wěn)定性。
- 支持多種限流模式:可以根據(jù) IP、用戶、接口總量等不同的維度進(jìn)行限流,滿足不同場(chǎng)景的需求。
- 基于滑動(dòng)窗口,計(jì)數(shù)更精確:Lua 腳本使用滑動(dòng)窗口算法來統(tǒng)計(jì)請(qǐng)求數(shù),相比固定窗口算法,計(jì)數(shù)更加精確,可以有效避免誤判。
- 使用 Lua 腳本保證原子性,避免競(jìng)態(tài)條件:Lua 腳本在 Redis 中是原子執(zhí)行的,確保了限流邏輯的原子性,避免了多個(gè)實(shí)例同時(shí)修改限流狀態(tài)時(shí)可能出現(xiàn)的競(jìng)態(tài)條件。
缺點(diǎn)
- 強(qiáng)依賴 Redis:該方案的實(shí)現(xiàn)依賴于 Redis,如果 Redis 出現(xiàn)故障,可能會(huì)影響系統(tǒng)的正常運(yùn)行。
- 實(shí)現(xiàn)復(fù)雜度較高:需要編寫 Lua 腳本并進(jìn)行 Redis 操作,實(shí)現(xiàn)復(fù)雜度相對(duì)較高,對(duì)開發(fā)人員的技術(shù)要求也較高。
4. 集成 Sentinel 實(shí)現(xiàn)接口防刷
阿里巴巴開源的 Sentinel 是一個(gè)強(qiáng)大的流量控制組件,它就像一個(gè)智能的“交通警察”,可以對(duì)系統(tǒng)的流量進(jìn)行實(shí)時(shí)監(jiān)控和控制,提供了豐富的限流、熔斷、系統(tǒng)保護(hù)等功能。
實(shí)現(xiàn)步驟
4.1 添加依賴
首先,我們需要在項(xiàng)目中添加 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 控制臺(tái)地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
# 取消 Sentinel 控制臺(tái)懶加載
spring.cloud.sentinel.eager=true
# 應(yīng)用名稱
spring.application.name=my-application在這個(gè)配置中,我們?cè)O(shè)置了 Sentinel 控制臺(tái)的地址,取消了控制臺(tái)的懶加載,并指定了應(yīng)用的名稱。
4.3 創(chuàng)建 Sentinel 配置
接下來,我們需要?jiǎng)?chuàng)建一個(gè) Sentinel 配置類,用于定義流控規(guī)則。以下是具體的代碼實(shí)現(xiàn):
圖片
在這個(gè)配置類中,我們創(chuàng)建了一個(gè) SentinelResourceAspect 實(shí)例,并在 init 方法中定義了流控規(guī)則。對(duì)于 /api/user 接口,我們?cè)O(shè)置了每秒允許 10 個(gè)請(qǐng)求的限流規(guī)則。對(duì)于 /api/order 接口,我們?cè)O(shè)置了每秒允許 5 個(gè)請(qǐng)求的限流規(guī)則,并開啟了預(yù)熱模式,預(yù)熱期為 10 秒。
4.4 創(chuàng)建 URL 資源解析器
為了讓 Sentinel 能夠正確識(shí)別請(qǐng)求的資源,我們需要?jiǎng)?chuàng)建一個(gè) URL 資源解析器。以下是具體的代碼實(shí)現(xiàn):
@Component
publicclass UrlCleaner implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 獲取請(qǐng)求的 URL 路徑
String path = request.getRequestURI();
// 可以添加更復(fù)雜的解析邏輯,例如:
// 1. 去除路徑變量:/api/user/123 -> /api/user/{id}
// 2. 添加請(qǐng)求方法前綴:GET:/api/user
return path;
}
}在這個(gè)解析器中,我們簡(jiǎn)單地返回請(qǐng)求的 URL 路徑,你可以根據(jù)實(shí)際需求添加更復(fù)雜的解析邏輯。
4.5 創(chuàng)建全局異常處理器
當(dāng)請(qǐng)求被 Sentinel 限流時(shí),會(huì)拋出 BlockException 異常,我們需要?jiǎng)?chuàng)建一個(gè)全局異常處理器來處理這個(gè)異常。以下是具體的代碼實(shí)現(xiàn):
@RestControllerAdvice
publicclass SentinelExceptionHandler {
@ExceptionHandler(BlockException.class)
public Result handleBlockException(BlockException e) {
String message = "請(qǐng)求過于頻繁,請(qǐng)稍后再試";
if (e instanceof FlowException) {
message = "接口限流:" + message;
} elseif (e instanceof DegradeException) {
message = "服務(wù)降級(jí):系統(tǒng)繁忙,請(qǐng)稍后再試";
} elseif (e instanceof ParamFlowException) {
message = "熱點(diǎn)參數(shù)限流:請(qǐng)求過于頻繁";
} elseif (e instanceof SystemBlockException) {
message = "系統(tǒng)保護(hù):系統(tǒng)資源不足";
} elseif (e instanceof AuthorityException) {
message = "授權(quán)控制:沒有訪問權(quán)限";
}
return Result.error(429, message);
}
}在這個(gè)異常處理器中,我們根據(jù)不同的 BlockException 類型返回不同的錯(cuò)誤信息,提示用戶請(qǐng)求被限流的原因。
4.6 使用 @SentinelResource 注解
為了讓 Sentinel 能夠?qū)涌谶M(jìn)行限流,我們需要在接口方法上使用 @SentinelResource 注解。以下是具體的代碼實(shí)現(xiàn):
@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("請(qǐng)求頻率過高,請(qǐng)稍后再試");
}
// 異常回退方法
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;
}
}在這個(gè)示例中,我們?cè)?nbsp;getUser 方法上使用了 @SentinelResource 注解,指定了資源名、限流處理方法和異常回退方法。當(dāng)請(qǐng)求被限流時(shí),會(huì)調(diào)用 getUserBlockHandler 方法進(jìn)行處理。當(dāng)方法執(zhí)行過程中出現(xiàn)異常時(shí),會(huì)調(diào)用 getUserFallback 方法進(jìn)行回退。
4.7 更復(fù)雜的限流規(guī)則配置
除了基本的流控規(guī)則,Sentinel 還支持更復(fù)雜的限流規(guī)則配置,如基于 QPS + 調(diào)用關(guān)系的限流規(guī)則、基于并發(fā)線程數(shù)的限流規(guī)則、熱點(diǎn)參數(shù)限流規(guī)則等。以下是具體的代碼實(shí)現(xiàn):
@Service
@Slf4j
publicclass SentinelRuleService {
public void initComplexFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 基于 QPS + 調(diào)用關(guān)系的限流規(guī)則
FlowRule apiRule = new FlowRule();
apiRule.setResource("/api/data");
apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
apiRule.setCount(20);
// 限制調(diào)用來源
apiRule.setLimitApp("frontend"); // 只限制來自前端應(yīng)用的調(diào)用
// 流控策略:關(guān)聯(lián)資源
apiRule.setStrategy(RuleConstant.STRATEGY_RELATE);
apiRule.setRefResource("/api/important"); // 當(dāng) important 接口 QPS 高時(shí),限制 data 接口
rules.add(apiRule);
// 基于并發(fā)線程數(shù)的限流
FlowRule threadRule = new FlowRule();
threadRule.setResource("/api/heavy-task");
threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 基于線程數(shù)
threadRule.setCount(5); // 最多 5 個(gè)線程同時(shí)處理
rules.add(threadRule);
// 加載規(guī)則
FlowRuleManager.loadRules(rules);
}
public void initHotspotRules() {
// 熱點(diǎn)參數(shù)限流規(guī)則
List<ParamFlowRule> rules = new ArrayList<>();
ParamFlowRule rule = new ParamFlowRule("/api/product");
// 對(duì)第 0 個(gè)參數(shù)(productId)進(jìn)行限流
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); // 更嚴(yán)格的限制
rule.setParamFlowItemList(Arrays.asList(item1, item2));
rules.add(rule);
ParamFlowRuleManager.loadRules(rules);
}
}在這個(gè)服務(wù)中,我們定義了基于 QPS + 調(diào)用關(guān)系的限流規(guī)則、基于并發(fā)線程數(shù)的限流規(guī)則和熱點(diǎn)參數(shù)限流規(guī)則,并使用 FlowRuleManager 和 ParamFlowRuleManager 加載這些規(guī)則。
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 功能全面:Sentinel 支持 QPS 限流、并發(fā)線程數(shù)限流、熱點(diǎn)參數(shù)限流等多種限流方式,還提供了熔斷、系統(tǒng)保護(hù)等功能,可以滿足不同場(chǎng)景的需求。
- 支持多種控制策略:可以選擇直接拒絕、預(yù)熱、排隊(duì)等多種控制策略,根據(jù)實(shí)際情況靈活調(diào)整限流行為。
- 提供控制臺(tái)可視化管理:Sentinel 提供了可視化的控制臺(tái),可以實(shí)時(shí)監(jiān)控系統(tǒng)的流量情況,方便進(jìn)行規(guī)則的配置和管理。
- 支持動(dòng)態(tài)規(guī)則調(diào)整:可以通過控制臺(tái)或 API 動(dòng)態(tài)調(diào)整限流規(guī)則,無需重啟應(yīng)用,提高了系統(tǒng)的靈活性和可維護(hù)性。
- 可與 Spring Cloud 體系無縫集成:Sentinel 可以與 Spring Cloud 體系無縫集成,方便在微服務(wù)架構(gòu)中使用。
缺點(diǎn)
- 學(xué)習(xí)曲線較陡峭:Sentinel 的功能豐富,配置復(fù)雜,對(duì)于初學(xué)者來說,學(xué)習(xí)成本較高。
- 分布式場(chǎng)景下需要額外配置規(guī)則持久化:在分布式場(chǎng)景下,需要額外配置規(guī)則持久化,確保不同實(shí)例之間的規(guī)則一致性。
- 引入了額外的依賴:集成 Sentinel 需要引入額外的依賴,增加了項(xiàng)目的復(fù)雜度和維護(hù)成本。
5. 驗(yàn)證碼與行為分析防刷
對(duì)于某些敏感操作,如登錄、注冊(cè)、支付等,單純的限流可能無法有效防止惡意請(qǐng)求。此時(shí),可以結(jié)合驗(yàn)證碼和行為分析來進(jìn)一步增強(qiáng)系統(tǒng)的安全性,有效區(qū)分人類用戶和自動(dòng)化腳本。
實(shí)現(xiàn)步驟
5.1 圖形驗(yàn)證碼實(shí)現(xiàn)
首先,我們需要添加圖形驗(yàn)證碼的依賴:
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>5.2 創(chuàng)建驗(yàn)證碼服務(wù)
接下來,我們需要?jiǎng)?chuàng)建一個(gè)驗(yàn)證碼服務(wù),用于生成和驗(yàn)證驗(yàn)證碼。以下是具體的代碼實(shí)現(xiàn):
@Service
publicclass CaptchaService {
@Autowired
private StringRedisTemplate redisTemplate;
privatestaticfinallong CAPTCHA_EXPIRE_TIME = 5 * 60; // 5 分鐘
/**
* 生成驗(yàn)證碼
* @param request HTTP 請(qǐng)求
* @param response HTTP 響應(yīng)
* @return 驗(yàn)證碼 Base64 字符串
*/
public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) {
// 生成驗(yàn)證碼
SpecCaptcha captcha = new SpecCaptcha(130, 48, 5);
// 生成驗(yàn)證碼 ID
String captchaId = UUID.randomUUID().toString();
// 將驗(yàn)證碼存入 Redis
redisTemplate.opsForValue().set(
"captcha:" + captchaId,
captcha.text().toLowerCase(),
CAPTCHA_EXPIRE_TIME,
TimeUnit.SECONDS
);
// 設(shè)置 Cookie
Cookie cookie = new Cookie("captchaId", captchaId);
cookie.setMaxAge((int) CAPTCHA_EXPIRE_TIME);
cookie.setPath("/");
response.addCookie(cookie);
// 返回 Base64 編碼的驗(yàn)證碼圖片
return captcha.toBase64();
}
/**
* 驗(yàn)證驗(yàn)證碼
* @param request HTTP 請(qǐng)求
* @param captchaCode 用戶輸入的驗(yàn)證碼
* @return 是否驗(yàn)證通過
*/
public boolean validateCaptcha(HttpServletRequest request, String captchaCode) {
// 從 Cookie 獲取驗(yàn)證碼 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 獲取正確的驗(yàn)證碼
String key = "captcha:" + captchaId;
String correctCode = redisTemplate.opsForValue().get(key);
// 驗(yàn)證成功后刪除驗(yàn)證碼
if (correctCode != null && correctCode.equals(captchaCode.toLowerCase())) {
redisTemplate.delete(key);
returntrue;
}
returnfalse;
}
}在這個(gè)服務(wù)中,我們使用 easy-captcha 庫生成驗(yàn)證碼,并將驗(yàn)證碼存入 Redis 中。在驗(yàn)證驗(yàn)證碼時(shí),我們從 Cookie 中獲取驗(yàn)證碼 ID,然后從 Redis 中獲取正確的驗(yàn)證碼進(jìn)行比對(duì)。
5.3 創(chuàng)建驗(yàn)證碼控制器
為了方便前端獲取驗(yàn)證碼,我們需要?jiǎng)?chuàng)建一個(gè)驗(yàn)證碼控制器。以下是具體的代碼實(shí)現(xiàn):
@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);
}
}在這個(gè)控制器中,我們提供了一個(gè) GET 請(qǐng)求接口,用于生成驗(yàn)證碼并返回 Base64 編碼的驗(yàn)證碼圖片。
5.4 創(chuàng)建驗(yàn)證碼注解
為了標(biāo)記需要進(jìn)行驗(yàn)證碼驗(yàn)證的接口方法,我們可以創(chuàng)建一個(gè)驗(yàn)證碼注解。以下是具體的代碼實(shí)現(xiàn):
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {
String captchaParam() default "captchaCode";
}在這個(gè)注解中,我們可以通過 captchaParam 屬性指定驗(yàn)證碼參數(shù)的名稱。
5.5 實(shí)現(xiàn)驗(yàn)證碼攔截器
接下來,我們需要實(shí)現(xiàn)一個(gè)驗(yàn)證碼攔截器,在請(qǐng)求進(jìn)入接口之前進(jìn)行驗(yàn)證碼驗(yàn)證。以下是具體的代碼實(shí)現(xiàn):
@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;
}
// 獲取驗(yàn)證碼參數(shù)
String captchaParam = captchaRequired.captchaParam();
String captchaCode = request.getParameter(captchaParam);
if (StringUtils.hasText(captchaCode)) {
// 驗(yàn)證驗(yàn)證碼
boolean valid = captchaService.validateCaptcha(request, captchaCode);
if (valid) {
returntrue;
}
}
// 驗(yàn)證失敗
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write("{\"code\":400,\"message\":\"驗(yàn)證碼錯(cuò)誤或已過期\"}");
returnfalse;
}
}在這個(gè)攔截器中,我們首先判斷請(qǐng)求的處理方法是否標(biāo)記了 @CaptchaRequired 注解。如果標(biāo)記了,則獲取驗(yàn)證碼參數(shù)并調(diào)用 CaptchaService 的 validateCaptcha 方法進(jìn)行驗(yàn)證。如果驗(yàn)證失敗,則返回一個(gè)錯(cuò)誤響應(yīng),提示用戶驗(yàn)證碼錯(cuò)誤或已過期。
5.6 創(chuàng)建行為分析服務(wù)
除了驗(yàn)證碼,我們還可以通過行為分析來檢測(cè)可疑的機(jī)器行為。以下是具體的代碼實(shí)現(xiàn):
@Service
@Slf4j
publicclass BehaviorAnalysisService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 檢查是否是可疑的機(jī)器行為
* @param request HTTP 請(qǐng)求
* @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. 檢查請(qǐng)求時(shí)間模式
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;
// 如果請(qǐng)求間隔非常均勻,可能是機(jī)器人
if (isUniformInterval(ip, interval)) {
log.warn("請(qǐng)求間隔異常均勻: IP={}, interval={}", ip, interval);
returntrue;
}
}
redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);
// 更多高級(jí)檢測(cè)邏輯...
returnfalse;
}
/**
* 檢查是否是機(jī)器人 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;
}
/**
* 檢查請(qǐng)求間隔是否異常均勻
*/
private boolean isUniformInterval(String ip, long interval) {
String key = "behavior:intervals:" + ip;
// 獲取最近的幾個(gè)間隔
List<String> intervalStrs = redisTemplate.opsForList().range(key, 0, 4);
redisTemplate.opsForList().leftPush(key, String.valueOf(interval));
redisTemplate.opsForList().trim(key, 0, 9); // 只保留最近 10 個(gè)
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
if (intervalStrs == null || intervalStrs.size() < 5) {
returnfalse;
}
// 計(jì)算間隔的方差,方差小說明請(qǐng)求間隔很均勻
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; // 方差閾值,需要根據(jù)實(shí)際情況調(diào)整
}
// getIpAddress 方法同上
}在這個(gè)服務(wù)中,我們從多個(gè)維度對(duì)請(qǐng)求進(jìn)行分析,包括訪問頻率、User-Agent 和請(qǐng)求時(shí)間模式。如果發(fā)現(xiàn)可疑的行為,我們會(huì)記錄日志并返回 true。
5.7 創(chuàng)建行為分析攔截器
最后,我們需要?jiǎng)?chuàng)建一個(gè)行為分析攔截器,在請(qǐng)求進(jìn)入接口之前進(jìn)行行為分析。以下是具體的代碼實(shí)現(xiàn):
@Component
publicclass BehaviorAnalysisInterceptor implements HandlerInterceptor {
@Autowired
private BehaviorAnalysisService behaviorAnalysisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 對(duì)于需要保護(hù)的端點(diǎn)進(jìn)行檢查
String path = request.getRequestURI();
if (path.startsWith("/api/") && isPotentialRiskEndpoint(path)) {
boolean suspicious = behaviorAnalysisService.isSuspicious(request);
if (suspicious) {
// 需要驗(yàn)證碼或其他額外驗(yàn)證
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{\"code\":429,\"message\":\"檢測(cè)到異常訪問,請(qǐng)進(jìn)行驗(yàn)證\",\"needCaptcha\":true}");
returnfalse;
}
}
returntrue;
}
/**
* 判斷是否是高風(fēng)險(xiǎn)端點(diǎn)
*/
private boolean isPotentialRiskEndpoint(String path) {
return path.contains("/login") ||
path.contains("/register") ||
path.contains("/payment") ||
path.contains("/order") ||
path.contains("/password");
}
}在這個(gè)攔截器中,我們首先判斷請(qǐng)求的 URI 是否以 /api/ 開頭,并且是否是高風(fēng)險(xiǎn)端點(diǎn)。如果是,則調(diào)用 BehaviorAnalysisService 的 isSuspicious 方法進(jìn)行行為分析。如果發(fā)現(xiàn)可疑行為,則返回一個(gè)錯(cuò)誤響應(yīng),提示用戶進(jìn)行驗(yàn)證。
5.8 使用示例
以下是一個(gè)使用驗(yàn)證碼和行為分析的示例:
@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) {
// 注冊(cè)邏輯
return userService.register(registerDTO);
}
}在這個(gè)示例中,我們?cè)?nbsp;login 和 register 方法上使用了 @CaptchaRequired 注解,要求用戶輸入驗(yàn)證碼進(jìn)行驗(yàn)證。
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 能有效區(qū)分人類用戶和自動(dòng)化腳本:通過驗(yàn)證碼和行為分析,可以準(zhǔn)確地識(shí)別出自動(dòng)化腳本的惡意請(qǐng)求,有效防止刷接口行為。
- 對(duì)惡意用戶有較強(qiáng)的阻止作用:驗(yàn)證碼和行為分析的雙重防護(hù),大大增加了惡意用戶的攻擊成本,使其難以得逞。
- 針對(duì)敏感操作提供額外安全層:對(duì)于登錄、注冊(cè)、支付等敏感操作,驗(yàn)證碼和行為分析可以提供額外的安全保障,保護(hù)用戶的隱私和財(cái)產(chǎn)安全。
- 可以實(shí)現(xiàn)自適應(yīng)安全策略:根據(jù)用戶的行為特征和系統(tǒng)的安全狀況,可以動(dòng)態(tài)調(diào)整驗(yàn)證碼和行為分析的策略,實(shí)現(xiàn)自適應(yīng)的安全防護(hù)。
缺點(diǎn)
- 增加了用戶操作成本,可能影響用戶體驗(yàn):要求用戶輸入驗(yàn)證碼會(huì)增加用戶的操作步驟,降低用戶體驗(yàn)。特別是對(duì)于一些頻繁操作的用戶,可能會(huì)感到厭煩。
- 實(shí)現(xiàn)復(fù)雜,需要前后端配合:驗(yàn)證碼和行為分析的實(shí)現(xiàn)涉及到前后端的多個(gè)環(huán)節(jié),需要前后端密切配合,增加了開發(fā)和維護(hù)的難度。
- 某些驗(yàn)證碼可能被 OCR 技術(shù)破解:雖然現(xiàn)在的驗(yàn)證碼技術(shù)不斷發(fā)展,但仍然存在被 OCR 技術(shù)破解的風(fēng)險(xiǎn),需要不斷更新和改進(jìn)驗(yàn)證碼的生成和驗(yàn)證方式。
- 行為分析可能產(chǎn)生誤判:行為分析是基于一定的規(guī)則和算法進(jìn)行的,可能會(huì)出現(xiàn)誤判的情況,將正常用戶的行為誤判為可疑行為,影響用戶的正常使用。
方案對(duì)比與選擇
方案 | 實(shí)現(xiàn)難度 | 防刷效果 | 分布式支持 | 用戶體驗(yàn) | 適用場(chǎng)景 |
基于注解的訪問頻率限制 | 低 | 中 | 需配合 Redis | 一般 | 一般接口,簡(jiǎn)單場(chǎng)景 |
令牌桶算法 | 中 | 中高 | 單機(jī) | 好 | 允許突發(fā)流量的場(chǎng)景 |
分布式限流(Redis+Lua) | 高 | 高 | 支持 | 一般 | 分布式系統(tǒng),精確限流 |
Sentinel | 中高 | 高 | 需額外配置 | 可配置 | 復(fù)雜系統(tǒng),多維度防護(hù) |
驗(yàn)證碼與行為分析 | 高 | 高 | 支持 | 較差 | 敏感操作,關(guān)鍵業(yè)務(wù) |
總結(jié)
接口防刷是一個(gè)系統(tǒng)性工程,需要綜合考慮安全性、用戶體驗(yàn)、性能開銷和運(yùn)維復(fù)雜度等多方面因素。本文介紹的 5 種方案各有優(yōu)缺點(diǎn),你可以根據(jù)實(shí)際需求靈活選擇和組合。
無論采用哪種方案,接口防刷都應(yīng)該遵循以下原則:
- 最小影響原則:盡量不影響正常用戶的體驗(yàn),確保系統(tǒng)在安全的前提下能夠高效運(yùn)行。
- 梯度防護(hù)原則:根據(jù)接口的重要程度和風(fēng)險(xiǎn)等級(jí),采用不同強(qiáng)度的防護(hù)措施,實(shí)現(xiàn)精準(zhǔn)防護(hù)。
- 可監(jiān)控原則:提供充分的監(jiān)控和告警機(jī)制,及時(shí)發(fā)現(xiàn)和處理異常情況,確保系統(tǒng)的安全性和穩(wěn)定性。
- 靈活調(diào)整原則:支持動(dòng)態(tài)調(diào)整防護(hù)參數(shù)和策略,根據(jù)系統(tǒng)的運(yùn)行情況和安全需求,及時(shí)做出調(diào)整。
通過合理實(shí)施接口防刷策略,可以有效提高系統(tǒng)的安全性和穩(wěn)定性,為用戶提供更好的服務(wù)體驗(yàn)。希望本文能對(duì)你有所幫助,讓你的系統(tǒng)在復(fù)雜的網(wǎng)絡(luò)環(huán)境中更加安全可靠!




































