▷ 优雅的接口防刷处理方式,看这一篇就够了

⌹ beat365官方 ⏱️ 2025-07-15 18:20:01 👤 admin 👁️‍🗨️ 2005 ❤️ 295
优雅的接口防刷处理方式,看这一篇就够了

文章目录

1、API接口防刷1.1、概念1.2、原理1.3、目的1.4、实现方案介绍

2、方案一2.1、自定义注解2.2、拦截器2.3、Redis配置类2.4、注册拦截器2.5、测试2.6、Lua脚本实现方案

3、方案二3.1、自定义注解3.2、切面类3.3、测试

4、限流算法介绍(了解)4.1、令牌桶算法4.2、漏桶算法

5、总结

1、API接口防刷

1.1、概念

顾名思义,就是要实现某个接口在某段时间内只能让某人访问指定次数,超出次数,就不让访问了

1.2、原理

在请求的时候,服务器通过 Redis 记录下你请求的次数,如果次数超过限制就不给访问在 Redis 保存的 Redis 是有时效性的,过期就会删除

1.3、目的

主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性

1.4、实现方案介绍

拦截器+自定义注解+RedisAOP+自定义注解+Redis

我这里准备了一份基础代码:https://gitee.com/colinWu_java/spring-boot-base.git 接下来我会在此主干代码基础上进行开发

2、方案一

好,接下来我们直接实战编码,运用到项目中的话,非常实用,使用起来也非常方便,下面是我们需要写的几个核心类

自定义注解拦截器(核心)Redis配置类(设置序列化用)

2.1、自定义注解

package org.wujiangbo.annotation;

import java.lang.annotation.*;

/**

* 用于防刷限流的注解

* 默认是5秒内只能调用一次

*/

@Target({ ElementType.METHOD })

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface RateLimit {

/** 限流的key */

String key() default "limit:";

/** 周期,单位是秒 */

int cycle() default 5;

/** 请求次数 */

int count() default 1;

/** 默认提示信息 */

String msg() default "请勿重复点击";

}

2.2、拦截器

package org.wujiangbo.interceptor;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import org.wujiangbo.annotation.RateLimit;

import org.wujiangbo.exception.MyException;

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.util.concurrent.TimeUnit;

/**

* 防刷限流的拦截器

* @author wujiangbo

* @date 2022-08-23 18:39

*/

@Component

public class RateLimitInterceptor implements HandlerInterceptor {

@Resource

private RedisTemplate redisTemplate;

@Override

public boolean preHandle(

HttpServletRequest request,

HttpServletResponse response,

Object handler) throws Exception {

// 如果请求的是方法,则需要做校验

if (handler instanceof HandlerMethod) {

HandlerMethod handlerMethod = (HandlerMethod) handler;

// 获取目标方法上是否有指定注解

RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);

if (rateLimit == null) {

//说明目标方法上没有 RateLimit 注解

return true;

}

//代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口

// 获取请求IP地址

String ip = getIpAddr(request);

// 请求url路径

String uri = request.getRequestURI();

//存到redis中的key

String key = "RateLimit:" + ip + ":" + uri;

// 缓存中存在key,在限定访问周期内已经调用过当前接口

if (redisTemplate.hasKey(key)) {

// 访问次数自增1

redisTemplate.opsForValue().increment(key, 1);

// 超出访问次数限制

if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {

throw new MyException(rateLimit.msg());

}

// 未超出访问次数限制,不进行任何操作,返回true

} else {

// 第一次设置数据,过期时间为注解确定的访问周期

redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);

}

return true;

}

//如果请求的不是方法,直接放行

return true;

}

//获取请求的归属IP地址

private String getIpAddr(HttpServletRequest request) {

String ipAddress = null;

try {

ipAddress = request.getHeader("x-forwarded-for");

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("WL-Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getRemoteAddr();

}

// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割

if (ipAddress != null && ipAddress.length() > 15) {

// = 15

if (ipAddress.indexOf(",") > 0) {

ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));

}

}

} catch (Exception e) {

ipAddress = "";

}

return ipAddress;

}

}

2.3、Redis配置类

package org.wujiangbo.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import com.fasterxml.jackson.annotation.PropertyAccessor;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.RedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

/**

* Redis配置类

*/

@Configuration

public class RedisConfig {

@Bean

public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {

// 设置序列化

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

ObjectMapper om = new ObjectMapper();

om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置redisTemplate

RedisTemplate redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(lettuceConnectionFactory);

RedisSerializer stringSerializer = new StringRedisSerializer();

redisTemplate.setKeySerializer(stringSerializer);// key序列化

redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化

redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化

redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化

redisTemplate.afterPropertiesSet();

return redisTemplate;

}

}

2.4、注册拦截器

package cn.wujiangbo.config;

import cn.wujiangbo.interceptor.RateLimitInterceptor;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**

* 配置拦截器

* @author wujiangbo

* @date 2022-08-23 18:51

*/

@Configuration

public class WebConfig extends WebMvcConfigurationSupport {

@Autowired

private RateLimitInterceptor rateLimitInterceptor;

@Override

protected void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(rateLimitInterceptor);

}

}

项目需要导入依赖:

org.springframework.boot

spring-boot-starter-data-redis

yml配置文件如下:

server:

port: 8001

undertow:

# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程

# 不要设置过大,如果过大,启动项目会报错:打开文件数过多(CPU有几核,就填写几)

io-threads: 6

# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程

# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是:io-threads * 8

worker-threads: 48

# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理

# 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可

buffer-size: 1024

# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region

buffers-per-region: 1024

# 是否分配的直接内存(NIO直接分配的堆外内存)

direct-buffers: true

spring:

#redis配置

redis:

# 数据库索引

database: 0

# 地址

host: 127.0.0.1

# 端口,默认为6379

port: 6379

# 密码

password: 123456

# 连接超时时间

timeout: 30s

jedis:

pool:

time-between-eviction-runs: 1000

max-active: 200

max-wait: -1ms

min-idle: 5

max-idle: 20

#配置数据库链接信息

datasource:

url: jdbc:mysql://127.0.0.1:3306/test1?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true

username: root

password: 123456

driver-class-name: com.mysql.jdbc.Driver

type: com.alibaba.druid.pool.DruidDataSource

application:

name: springboot #服务名

#MyBatis-Plus相关配置

mybatis-plus:

#指定Mapper.xml路径,如果与Mapper路径相同的话,可省略

mapper-locations: classpath:org/wujiangbo/mapper/*Mapper.xml

configuration:

map-underscore-to-camel-case: true #开启驼峰大小写自动转换

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启控制台sql输出

2.5、测试

package org.wujiangbo.controller;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import org.wujiangbo.annotation.CheckPermission;

import org.wujiangbo.annotation.RateLimit;

import org.wujiangbo.result.JSONResult;

/**

* @desc 测试接口类

* @author 波波老师(weixin:javabobo0513)

*/

@RestController

@Slf4j

public class TestController {

//5秒内只能访问2次

@RateLimit(key= "testLimit", count = 2, cycle = 6, msg = "同志,不要请求这么快,好吗")

@GetMapping("/test001")

public JSONResult rate() {

System.out.println("成功发送一条短信");

return JSONResult.success();

}

}

打开浏览器访问:http://localhost:8081/test/test001

开始返回结果:

但是当你多刷几次后,就显示报错信息了:

很显然,我们接口防刷功能就实现了,测试成功

以上代码已经全部提交到:https://gitee.com/colinWu_java/spring-boot-base.git 在分支【InterfacePreventAttack】中

2.6、Lua脚本实现方案

针对上面方案我们可以改用lua脚本实现,关于lua介绍:

lua本身就是一种编程语言,是一个小巧的脚本语言性能非常高

我们在Redis的场景中使用lua脚本有以下好处:

减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用

这里我们的需求是做限流,思路是根据用户的IP和访问的URI来进行计数,达到一定数量之后进行限制访问。这应该是限流操作的计算法,另外还有令牌算法和漏桶算法

我们这里介绍最简单的计算法,首先我们在项目的resources目录下新建limit.lua文件,里面内容如下:

local key = "rate.limit:" .. KEYS[1] --限流KEY

local limit = tonumber(ARGV[1]) --限流大小

local cycle = ARGV[2] --过期周期

local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then --如果超出限流大小

return 0

else --请求数+1,并设置过期时间

redis.call("INCRBY", key, "1")

redis.call("expire", key, cycle)

return current + 1

end

上面就是我们的lua限流脚本

然后我们Redis配置类中新增下面方法,用来读取上面的lua脚本:

@Bean

public DefaultRedisScript redisluaScript() {

DefaultRedisScript redisScript = new DefaultRedisScript<>();

//读取 lua 脚本

redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));

redisScript.setResultType(Number.class);

return redisScript;

}

然后拦截器类代码改成下面这样了:

package cn.wujiangbo.interceptor;

import cn.wujiangbo.annotation.RateLimit;

import cn.wujiangbo.exception.MyException;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.time.LocalDateTime;

import java.time.format.DateTimeFormatter;

import java.util.Collections;

import java.util.List;

/**

* 防刷限流的拦截器

* @author wujiangbo

* @date 2022-08-23 18:39

*/

@Component

public class RateLimitInterceptor implements HandlerInterceptor {

@Resource

private RedisTemplate redisTemplate;

@Autowired

private DefaultRedisScript redisLuaScript;

@Override

public boolean preHandle(

HttpServletRequest request,

HttpServletResponse response,

Object handler) throws Exception {

// 如果请求的是方法,则需要做校验

if (handler instanceof HandlerMethod) {

HandlerMethod handlerMethod = (HandlerMethod) handler;

// 获取目标方法上是否有指定注解

RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);

if (rateLimit == null) {

//说明目标方法上没有 RateLimit 注解

return true;

}

//代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口

// 获取请求IP地址

String ip = getIpAddr(request);

// 请求url路径

String uri = request.getRequestURI();

//存到redis中的key

String key = rateLimit.key() + ip + ":" + uri;

//将key转成List类型

List keys = Collections.singletonList(key);

Number number = redisTemplate.execute(redisLuaScript, keys, rateLimit.count(), rateLimit.cycle());

if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {

System.out.println(rateLimit.cycle() + "秒内访问第:" + number.toString() + " 次" + getCurrentTime());

return true;

}

throw new MyException(rateLimit.msg());

}

//如果请求的不是方法,直接放行

return true;

}

//获取当前时间

public static String getCurrentTime(){

LocalDateTime localDateTime = LocalDateTime.now();

return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

}

//获取请求的归属IP地址

private String getIpAddr(HttpServletRequest request) {

String ipAddress = null;

try {

ipAddress = request.getHeader("x-forwarded-for");

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("WL-Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getRemoteAddr();

}

// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割

if (ipAddress != null && ipAddress.length() > 15) {

// = 15

if (ipAddress.indexOf(",") > 0) {

ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));

}

}

} catch (Exception e) {

ipAddress = "";

}

return ipAddress;

}

}

测试代码:

package cn.wujiangbo.controller;

import cn.wujiangbo.annotation.RateLimit;

import cn.wujiangbo.result.JSONResult;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**

* 测试接口

* @author wujiangbo

* @date 2022-08-23 18:50

*/

@RestController

@RequestMapping("/test")

public class TestController {

//4秒内只能访问2次

@RateLimit(key= "testLimit", count = 2, cycle = 4, msg = "同志,不要请求这么快,好吗")

@GetMapping("/test001")

public JSONResult rate() {

System.out.println("成功发送一条短信");

return JSONResult.success();

}

}

然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了

3、方案二

AOP方案需要导入依赖:

org.springframework.boot

spring-boot-starter-aop

3.1、自定义注解

package cn.wujiangbo.annotation;

import java.lang.annotation.*;

/**

* 用于防刷限流的注解

* 默认是5秒内只能调用一次

* @author wujiangbo

* @date 2022-08-23 18:36

*/

@Target({ ElementType.METHOD })

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface RateLimit {

/** 限流的key */

String key() default "limit:";

/** 周期,单位是秒 */

int cycle() default 5;

/** 请求次数 */

int count() default 1;

/** 默认提示信息 */

String msg() default "请勿重复点击";

}

3.2、切面类

package cn.wujiangbo.aspect;

import cn.wujiangbo.annotation.RateLimit;

import cn.wujiangbo.exception.MyException;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.Method;

import java.util.concurrent.TimeUnit;

/**

* 切面类:实现限流校验

* @author wujiangbo

* @date 2022-08-24 11:27

*/

@Aspect

@Component

public class AccessLimitAspect {

@Resource

private RedisTemplate redisTemplate;

/**

* 这里我们使用注解的形式

* 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method

*/

@Pointcut("@annotation(cn.wujiangbo.annotation.RateLimit)")

public void limitPointCut() {

}

/**

* 环绕通知

*/

@Around("limitPointCut()")

public Object around(ProceedingJoinPoint pjp) throws Throwable {

// 获取被注解的方法

MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;

MethodSignature signature = (MethodSignature) mjp.getSignature();

Method method = signature.getMethod();

// 获取方法上的注解

RateLimit rateLimit = method.getAnnotation(RateLimit.class);

if (rateLimit == null) {

// 如果没有注解,则继续调用,不做任何处理

return pjp.proceed();

}

/**

* 代码走到这里,说明有 RateLimit 注解,那么就需要做限流校验了

* 1、这里可以使用Redis的API做计数校验

* 2、这里也可以使用Lua脚本做计数校验,都可以

*/

//获取request对象

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

// 获取请求IP地址

String ip = getIpAddr(request);

// 请求url路径

String uri = request.getRequestURI();

//存到redis中的key

String key = "RateLimit:" + ip + ":" + uri;

// 缓存中存在key,在限定访问周期内已经调用过当前接口

if (redisTemplate.hasKey(key)) {

// 访问次数自增1

redisTemplate.opsForValue().increment(key, 1);

// 超出访问次数限制

if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {

throw new MyException(rateLimit.msg());

}

// 未超出访问次数限制,不进行任何操作,返回true

} else {

// 第一次设置数据,过期时间为注解确定的访问周期

redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);

}

return pjp.proceed();

}

//获取请求的归属IP地址

private String getIpAddr(HttpServletRequest request) {

String ipAddress = null;

try {

ipAddress = request.getHeader("x-forwarded-for");

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("WL-Proxy-Client-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getRemoteAddr();

}

// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割

if (ipAddress != null && ipAddress.length() > 15) {

// = 15

if (ipAddress.indexOf(",") > 0) {

ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));

}

}

} catch (Exception e) {

ipAddress = "";

}

return ipAddress;

}

}

3.3、测试

package cn.wujiangbo.controller;

import cn.wujiangbo.annotation.RateLimit;

import cn.wujiangbo.result.JSONResult;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**

* 测试接口

* @author wujiangbo

* @date 2022-08-23 18:50

*/

@RestController

@RequestMapping("/test")

public class TestController {

//4秒内只能访问2次

@RateLimit(key= "testLimit", count = 2, cycle = 4, msg = "同志,不要请求这么快,好吗")

@GetMapping("/test001")

public JSONResult rate() {

System.out.println("成功发送一条短信");

return JSONResult.success();

}

}

然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了

4、限流算法介绍(了解)

4.1、令牌桶算法

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待

4.2、漏桶算法

漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照固定频率从队列头取出请求进行处理如果请求量大,则会导致队列满,那么新来的请求就会被抛弃

5、总结

实际项目中,接口防刷是一个非常普遍的需求一般的处理方案都是采用自定义注解+拦截器+Redis处理的

◈ 相关文章

轻松更改微博用户名的详细步骤与常见疑问解答
⌹ 365提款失败怎么办方案

▷ 轻松更改微博用户名的详细步骤与常见疑问解答

⏱️ 06-27 👁️‍🗨️ 1139
疾速追杀2
⌹ beat365官方

▷ 疾速追杀2

⏱️ 06-29 👁️‍🗨️ 9311
移动宽带账号和密码怎么查?教你3种方法
⌹ 365提款失败怎么办方案

▷ 移动宽带账号和密码怎么查?教你3种方法

⏱️ 07-12 👁️‍🗨️ 3429