接口防刷处理方案


1、    API接口防刷     1.1、概念     
顾名思义,就是要实现某个接口在某段时间内只能让某人访问指定次数,超出次数,就不让访问了 1.2、原理     
  在请求的时候,服务器通过 Redis 记录下你请求的次数,如果次数超过限制就不给访问在 Redis 保存的 Redis 是有时效性的,过期就会删除
1.3、目的     
主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性
1.4、实现方案介绍     
1.    拦截器+自定义注解+Redis
2.    AOP+自定义注解+Redis
2、方案一     
好,接下来我们直接实战编码,运用到项目中的话,非常实用,使用起来也非常方便,下面是我们需要
写的几个核心类
1.    自定义注解
2.    拦截器(核心)
3.    Redis配置类(设置序列化用)
2.1、自定义注解     
 
2.2、拦截器     
 

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;
        }
        //如果请求的不是方法,直接放行
 
2.3、Redis配置类     
 
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、注册拦截器     
 
2.5、测试     
 
打开浏览器访问:http://localhost:8081/test/test001 开始返回结果:
 
但是当你多刷几次后,就显示报错信息了:
 
很显然,我们接口防刷功能就实现了,测试成功
2.6、Lua脚本实现方案     
针对上面方案我们可以改用lua脚本实现,关于lua介绍:
     lua本身就是一种编程语言,是一个小巧的脚本语言
性能非常高
我们在Redis的场景中使用lua脚本有以下好处:
1.    减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输
2.    原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务
3.    复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用
这里我们的需求是做限流,思路是根据用户的IP和访问的URI来进行计数,达到一定数量之后进行限制访
问。这应该是限流操作的计算法,另外还有令牌算法和漏桶算法
我们这里介绍最简单的计算法,首先我们在项目的resources目录下新建limit.lua文件,里面内容如下:
 
上面就是我们的lua限流脚本
然后我们Redis配置类中新增下面方法,用来读取上面的lua脚本:
 
然后拦截器类代码改成下面这样了:
 

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());
 
测试代码:
 
 
然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了
3、方案二     
AOP方案需要导入依赖:
 
3.1、自定义注解     
 
 
3.2、切面类     
 

    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");
            }
 
3.3、测试     
 
然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了
4、限流算法介绍(了解)     
4.1、令牌桶算法     
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌
桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝
当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待
4.2、漏桶算法     
漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照
固定频率从队列头取出请求进行处理
如果请求量大,则会导致队列满,那么新来的请求就会被抛弃
5、总结     
1.    实际项目中,接口防刷是一个非常普遍的需求
2.    一般的处理方案都是采用自定义注解+拦截器+Redis处理的
 

你可能感兴趣的:(总结,个人积累,接口,servlet,java,开发语言)