SpringBoot實(shí)現(xiàn)接口防刷的五種方案
1. 基于注解的訪問(wèn)頻率限制
最常見(jiàn)的防刷方案是通過(guò)自定義注解和AOP切面實(shí)現(xiàn)訪問(wèn)頻率限制。這種方法簡(jiǎn)單易用,實(shí)現(xiàn)成本低。
實(shí)現(xiàn)步驟
1.1 創(chuàng)建限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限制時(shí)間段,單位為秒
*/
int time() default 60;
/**
* 在限制時(shí)間段內(nèi)允許的最大請(qǐng)求次數(shù)
*/
int count() default 10;
/**
* 限流的key,支持SpEL表達(dá)式
*/
String key() default "";
/**
* 提示信息
*/
String message() default "操作太頻繁,請(qǐng)稍后再試";
}
1.2 實(shí)現(xiàn)限流切面
@Aspect
@Component
@Slf4j
public class 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)求的類(lèi)名
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) {
throw new RuntimeException(rateLimit.message());
}
// 執(zhí)行目標(biāo)方法
return pjp.proceed();
}
private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {
// 獲取用戶(hù)自定義的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)使用類(lèi)名+方法名+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);
// 如果是第一次訪問(wèn),設(shè)置過(guò)期時(shí)間
if (currentCount == 1) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return currentCount > count;
} catch (Exception e) {
log.error("限流異常", e);
return false;
}
}
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;
}
}
1.3 使用示例
@RestController
@RequestMapping("/api")
public class UserController {
@RateLimit(time = 60, count = 3, message = "請(qǐng)求太頻繁,請(qǐng)稍后再試")
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
// 使用SpEL表達(dá)式指定key
@RateLimit(time = 60, count = 1, key = "#id + '_' + #request.remoteAddr")
@PostMapping("/user/{id}/update")
public Result updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO, HttpServletRequest request) {
return userService.updateUser(id, userDTO);
}
}
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 實(shí)現(xiàn)簡(jiǎn)單,上手容易,單機(jī)情況下可以去掉Redis換成本地緩存實(shí)現(xiàn)
- 注解式使用,對(duì)業(yè)務(wù)代碼無(wú)侵入
- 可以精確控制接口粒度
- 支持靈活的限流策略配置
缺點(diǎn):
- 限流邏輯相對(duì)簡(jiǎn)單,無(wú)法應(yīng)對(duì)復(fù)雜場(chǎng)景
- 缺少預(yù)警機(jī)制
2. 令牌桶算法實(shí)現(xiàn)限流
令牌桶算法是一種更加靈活的限流算法,可以允許突發(fā)流量,同時(shí)又能限制長(zhǎng)期的平均流量。
實(shí)現(xiàn)步驟
2.1 引入依賴(lài)
Google提供的Guava庫(kù)中包含了令牌桶實(shí)現(xiàn):
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
2.2 創(chuàng)建令牌桶限流器
@Component
public class RateLimiter {
// 使用ConcurrentHashMap存儲(chǔ)不同接口的令牌桶
private final 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);
}
}
2.3 創(chuàng)建攔截器
@Component
public class TokenBucketInterceptor implements HandlerInterceptor {
@Autowired
private RateLimiter rateLimiter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 僅對(duì)API請(qǐng)求進(jìn)行限流
String requestURI = request.getRequestURI();
if (!requestURI.startsWith("/api/")) {
return true;
}
// 獲取IP地址作為限流鍵
String ip = getIpAddress(request);
String key = ip + ":" + requestURI;
// 嘗試獲取令牌,設(shè)置每秒2個(gè)請(qǐng)求的速率,等待100毫秒
boolean acquired = rateLimiter.tryAcquire(key, 2.0, 100, TimeUnit.MILLISECONDS);
if (!acquired) {
// 獲取失敗,返回限流響應(yīng)
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{"code":429,"message":"請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試"}");
return false;
}
return true;
}
// getIpAddress方法同上
}
2.4 配置攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenBucketInterceptor tokenBucketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenBucketInterceptor)
.addPathPatterns("/**");
}
}
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 支持突發(fā)流量,不會(huì)完全拒絕短時(shí)高峰
- 平滑的限流效果,用戶(hù)體驗(yàn)更好
- 可以配置不同接口的不同限流策略
- 無(wú)需額外的存儲(chǔ)設(shè)施
缺點(diǎn):
- 只適用于單機(jī)部署,分布式環(huán)境需要額外改造
- 重啟應(yīng)用后狀態(tài)丟失
- 無(wú)法精確控制時(shí)間窗口內(nèi)的請(qǐng)求總量
3. 分布式限流(Redis + Lua腳本)
對(duì)于分布式系統(tǒng),單機(jī)限流方案難以滿(mǎn)足需求。利用Redis和Lua腳本可以實(shí)現(xiàn)高效的分布式限流。
實(shí)現(xiàn)步驟
3.1 定義Lua腳本
創(chuàng)建一個(gè)Redis限流的Lua腳本,放在resources目錄下的scripts/rate_limiter.lua:
-- 限流Key
local key = KEYS[1]
-- 限流窗口,單位秒
local window = tonumber(ARGV[1])
-- 限流閾值
local threshold = tonumber(ARGV[2])
-- 當(dāng)前時(shí)間戳
local now = tonumber(ARGV[3])
-- 移除過(guò)期的請(qǐng)求記錄
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
-- 獲取當(dāng)前窗口內(nèi)的請(qǐng)求數(shù)
local count = redis.call('ZCARD', key)
-- 如果請(qǐng)求數(shù)超過(guò)閾值,拒絕請(qǐng)求
if count >= threshold then
return 0
end
-- 添加當(dāng)前請(qǐng)求記錄
redis.call('ZADD', key, now, now .. '-' .. math.random())
-- 設(shè)置過(guò)期時(shí)間
redis.call('EXPIRE', key, window)
-- 返回當(dāng)前窗口剩余可用請(qǐng)求數(shù)
return threshold - count - 1
3.2 創(chuàng)建Redis限流服務(wù)
@Service
@Slf4j
public class 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);
}
/**
* 嘗試獲取訪問(wèn)權(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í)行l(wèi)ua腳本
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;
}
}
}
3.3 創(chuàng)建分布式限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedRateLimit {
/**
* 限流的key前綴
*/
String prefix() default "rate:";
/**
* 時(shí)間窗口,單位秒
*/
int window() default 60;
/**
* 在時(shí)間窗口內(nèi)允許的最大請(qǐng)求數(shù)
*/
int threshold() default 10;
/**
* 限流模式: ip - 按IP限流, user - 按用戶(hù)限流, all - 接口總體限流
*/
String mode() default "ip";
}
3.4 實(shí)現(xiàn)分布式限流切面
@Aspect
@Component
@Slf4j
public class 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) {
throw new RuntimeException("接口訪問(wèn)過(guò)于頻繁,請(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":
// 按用戶(hù)限流
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)前用戶(hù)ID,根據(jù)實(shí)際認(rèn)證系統(tǒng)實(shí)現(xiàn)
private Object getUserId() {
// 這里簡(jiǎn)化處理,實(shí)際中應(yīng)從認(rèn)證信息中獲取
// 例如:SecurityContextHolder.getContext().getAuthentication().getPrincipal()
return null;
}
}
3.5 使用示例
@RestController
@RequestMapping("/api")
public class 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() {
// 查詢(xún)產(chǎn)品列表
return productService.findAll();
}
@DistributedRateLimit(window = 1, threshold = 100, mode = "all")
@GetMapping("/hot/resource")
public Resource getHotResource() {
// 獲取熱門(mén)資源
return resourceService.getHotResource();
}
}
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 適用于分布式系統(tǒng),多實(shí)例間共享限流狀態(tài)
- 支持多種限流模式:按IP、用戶(hù)、接口總量等
- 基于滑動(dòng)窗口,計(jì)數(shù)更精確
- 使用Lua腳本保證原子性,避免競(jìng)態(tài)條件
缺點(diǎn):
- 強(qiáng)依賴(lài)Redis
- 實(shí)現(xiàn)復(fù)雜度較高
4. 集成Sentinel實(shí)現(xiàn)接口防刷
阿里巴巴開(kāi)源的Sentinel是一個(gè)強(qiáng)大的流量控制組件,提供了豐富的限流、熔斷、系統(tǒng)保護(hù)等功能。
實(shí)現(xiàn)步驟
4.1 添加依賴(lài)
<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 控制臺(tái)地址 spring.cloud.sentinel.transport.dashboard=localhost:8080 # 取消Sentinel控制臺(tái)懶加載 spring.cloud.sentinel.eager=true # 應(yīng)用名稱(chēng) spring.application.name=my-application
4.3 創(chuàng)建Sentinel配置
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
@PostConstruct
public void init() {
// 定義流控規(guī)則
initFlowRules();
}
private void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 為/api/user接口設(shè)置流控規(guī)則
FlowRule userRule = new FlowRule();
userRule.setResource("/api/user");
userRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 基于QPS限流
userRule.setCount(10); // 每秒允許10個(gè)請(qǐng)求
rules.add(userRule);
// 為/api/order接口設(shè)置流控規(guī)則
FlowRule orderRule = new FlowRule();
orderRule.setResource("/api/order");
orderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
orderRule.setCount(5); // 每秒允許5個(gè)請(qǐng)求
orderRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 預(yù)熱模式
orderRule.setWarmUpPeriodSec(10); // 10秒預(yù)熱期
rules.add(orderRule);
// 加載規(guī)則
FlowRuleManager.loadRules(rules);
}
}
4.4 創(chuàng)建URL資源解析器
@Component
public class 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;
}
}
4.5 創(chuàng)建全局異常處理器
@RestControllerAdvice
public class SentinelExceptionHandler {
@ExceptionHandler(BlockException.class)
public Result handleBlockException(BlockException e) {
String message = "請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試";
if (e instanceof FlowException) {
message = "接口限流:" + message;
} else if (e instanceof DegradeException) {
message = "服務(wù)降級(jí):系統(tǒng)繁忙,請(qǐng)稍后再試";
} else if (e instanceof ParamFlowException) {
message = "熱點(diǎn)參數(shù)限流:請(qǐng)求過(guò)于頻繁";
} else if (e instanceof SystemBlockException) {
message = "系統(tǒng)保護(hù):系統(tǒng)資源不足";
} else if (e instanceof AuthorityException) {
message = "授權(quán)控制:沒(méi)有訪問(wèn)權(quán)限";
}
return Result.error(429, message);
}
}
4.6 使用@SentinelResource注解
@RestController
@RequestMapping("/api")
public class 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);
throw new RuntimeException("請(qǐng)求頻率過(guò)高,請(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;
}
}
4.7 更復(fù)雜的限流規(guī)則配置
@Service
@Slf4j
public class 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)用來(lái)源
apiRule.setLimitApp("frontend"); // 只限制來(lái)自前端應(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);
}
}
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 功能全面,支持QPS限流、并發(fā)線程數(shù)限流、熱點(diǎn)參數(shù)限流等
- 支持多種控制策略:直接拒絕、預(yù)熱、排隊(duì)等
- 提供控制臺(tái)可視化管理
- 支持動(dòng)態(tài)規(guī)則調(diào)整
- 可與Spring Cloud體系無(wú)縫集成
缺點(diǎn):
- 學(xué)習(xí)曲線較陡峭
- 分布式場(chǎng)景下需要額外配置規(guī)則持久化
- 引入了額外的依賴(lài)
5. 驗(yàn)證碼與行為分析防刷
對(duì)于某些敏感操作(如登錄、注冊(cè)、支付等),可以結(jié)合驗(yàn)證碼和行為分析來(lái)防止惡意請(qǐng)求。
實(shí)現(xiàn)步驟
5.1 圖形驗(yàn)證碼實(shí)現(xiàn)
首先添加依賴(lài):
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
5.2 創(chuàng)建驗(yàn)證碼服務(wù)
@Service
public class CaptchaService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final long 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 用戶(hù)輸入的驗(yàn)證碼
* @return 是否驗(yàn)證通過(guò)
*/
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) {
return false;
}
// 從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);
return true;
}
return false;
}
}
5.3 創(chuàng)建驗(yà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);
}
}
5.4 創(chuàng)建驗(yàn)證碼注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {
String captchaParam() default "captchaCode";
}
5.5 實(shí)現(xiàn)驗(yàn)證碼攔截器
@Component
public class CaptchaInterceptor implements HandlerInterceptor {
@Autowired
private CaptchaService captchaService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
CaptchaRequired captchaRequired = handlerMethod.getMethodAnnotation(CaptchaRequired.class);
if (captchaRequired == null) {
return true;
}
// 獲取驗(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) {
return true;
}
}
// 驗(yàn)證失敗
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write("{"code":400,"message":"驗(yàn)證碼錯(cuò)誤或已過(guò)期"}");
return false;
}
}
5.6 創(chuàng)建行為分析服務(wù)
@Service
@Slf4j
public class BehaviorAnalysisService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 檢查是否是可疑的機(jī)器行為
* @param request HTTP請(qǐng)求
* @return 是否可疑
*/
public boolean isSuspicious(HttpServletRequest request) {
// 1. 獲取客戶(hù)端信息
String ip = getIpAddress(request);
String userAgent = request.getHeader("User-Agent");
String requestId = request.getSession().getId();
// 2. 檢查訪問(wèn)頻率
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("訪問(wèn)頻率異常: IP={}, count={}", ip, count);
return true;
}
// 3. 檢查User-Agent
if (userAgent == null || isBotuserAgent(userAgent)) {
log.warn("可疑的User-Agent: {}", userAgent);
return true;
}
// 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);
return true;
}
}
redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);
// 更多高級(jí)檢測(cè)邏輯...
return false;
}
/**
* 檢查是否是機(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) {
return false;
}
// 計(jì)算間隔的方差,方差小說(shuō)明請(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方法同上
}
5.7 創(chuàng)建行為分析攔截器
@Component
public class 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è)到異常訪問(wèn),請(qǐng)進(jìn)行驗(yàn)證","needCaptcha":true}");
return false;
}
}
return true;
}
/**
* 判斷是否是高風(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");
}
}
5.8 使用示例
@RestController
@RequestMapping("/api")
public class 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);
}
}
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
- 能有效區(qū)分人類(lèi)用戶(hù)和自動(dòng)化腳本
- 對(duì)惡意用戶(hù)有較強(qiáng)的阻止作用
- 針對(duì)敏感操作提供額外安全層
- 可以實(shí)現(xiàn)自適應(yīng)安全策略
缺點(diǎn):
- 增加了用戶(hù)操作成本,可能影響用戶(hù)體驗(yàn)
- 實(shí)現(xiàn)復(fù)雜,需要前后端配合
- 某些驗(yàn)證碼可能被OCR技術(shù)破解
- 行為分析可能產(chǎn)生誤判
方案對(duì)比與選擇
| 方案 | 實(shí)現(xiàn)難度 | 防刷效果 | 分布式支持 | 用戶(hù)體驗(yàn) | 適用場(chǎng)景 |
|---|---|---|---|---|---|
| 基于注解的訪問(wèn)頻率限制 | 低 | 中 | 需配合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)性工程,需要考慮多方面因素:安全性、用戶(hù)體驗(yàn)、性能開(kāi)銷(xiāo)和運(yùn)維復(fù)雜度等。本文介紹的5種方案各有優(yōu)缺點(diǎn),可以根據(jù)實(shí)際需求靈活選擇和組合。
無(wú)論采用哪種方案,接口防刷都應(yīng)該遵循以下原則:
- 最小影響原則:盡量不影響正常用戶(hù)的體驗(yàn)
- 梯度防護(hù)原則:根據(jù)接口的重要程度采用不同強(qiáng)度的防護(hù)措施
- 可監(jiān)控原則:提供充分的監(jiān)控和告警機(jī)制
- 靈活調(diào)整原則:支持動(dòng)態(tài)調(diào)整防護(hù)參數(shù)和策略
通過(guò)合理實(shí)施接口防刷策略,可以有效提高系統(tǒng)的安全性和穩(wěn)定性,為用戶(hù)提供更好的服務(wù)體驗(yàn)。
以上就是SpringBoot實(shí)現(xiàn)接口防刷的五種方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot接口防刷的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
新版idea如何開(kāi)啟多臺(tái)JVM虛擬機(jī)的流程步驟
在IntelliJ?IDEA這個(gè)集成開(kāi)發(fā)環(huán)境中(IDE),開(kāi)啟JVM(Java?Virtual?Machine)通常是在運(yùn)行Java應(yīng)用程序時(shí)的操作,本文給大家介紹了新版idea如何開(kāi)啟多臺(tái)JVM虛擬機(jī)的流程步驟,需要的朋友可以參考下2024-10-10
IntelliJ IDEA中加速M(fèi)aven編譯的終極指南
這篇文章主要為大家詳細(xì)介紹了IntelliJ IDEA中加速M(fèi)aven編譯的終極指南,從 7 分鐘提速到 30 秒,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-10-10
springboot webflux 過(guò)濾器(使用RouterFunction實(shí)現(xiàn))
這篇文章主要介紹了springboot webflux 過(guò)濾器(使用RouterFunction實(shí)現(xiàn)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Mybatis動(dòng)態(tài)拼接sql提高插入速度實(shí)例
這篇文章主要介紹了Mybatis動(dòng)態(tài)拼接sql提高插入速度實(shí)例,當(dāng)數(shù)據(jù)量少的時(shí)候,沒(méi)問(wèn)題,有效時(shí)間內(nèi)可能完成插入,但是當(dāng)數(shù)據(jù)量達(dá)到一定程度的時(shí)候,每次都一個(gè)sql插入超時(shí),所以采用了拼接sql的方式加快速度,需要的朋友可以參考下2023-09-09
啟用設(shè)置org.slf4j.Logger打印并輸出日志方式
SpringBoot動(dòng)態(tài)生成接口實(shí)現(xiàn)流程示例講解

