接口防刷处理方案,太优雅了!

Posted Java技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口防刷处理方案,太优雅了!相关的知识,希望对你有一定的参考价值。

来源:juejin.cn/post/7200366809407750181


前言

本文为描述通过Interceptor以及Redis实现接口访问防刷Demo

这里会通过逐步找问题,逐步去完善的形式展示

原理

  • 通过ip地址+uri拼接用以作为访问者访问接口区分
  • 通过在Interceptor中拦截请求,从Redis中统计用户访问接口次数从而达到接口防刷目的

如下图所示

工程

推荐一个开源免费的 Spring Boot 实战项目:

https://github.com/javastacks/spring-boot-best-practice

其中,Interceptor处代码处理逻辑最为重要

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor 
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 多长时间内
     */
    @Value("$interfaceAccess.second")
    private Long second = 10L;

    /**
     * 访问次数
     */
    @Value("$interfaceAccess.time")
    private Long time = 3L;

    /**
     * 禁用时长--单位/秒
     */
    @Value("$interfaceAccess.lockTime")
    private Long lockTime = 60L;

    /**
     * 锁住时的key前缀
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 统计次数时的key前缀
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 

        String uri = request.getRequestURI();
        String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址
        String lockKey = LOCK_PREFIX + ip + uri;
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        if(Objects.isNull(isLock))
            // 还未被禁用
            String countKey = COUNT_PREFIX + ip + uri;
            Object count = redisTemplate.opsForValue().get(countKey);
            if(Objects.isNull(count))
                // 首次访问
                log.info("首次访问");
                redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);
            else
                // 此用户前一点时间就访问过该接口
                if((Integer)count < time)
                    // 放行,访问次数 + 1
                    redisTemplate.opsForValue().increment(countKey);
                else
                    log.info("禁用访问",ip, uri);
                    // 禁用
                    redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);
                    // 删除统计
                    redisTemplate.delete(countKey);
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                
            
        else
            // 此用户访问此接口已被禁用
            throw new CommonException(ResultCode.ACCESS_FREQUENT);
        
        return true;
    

在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置

当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理 (这里代码写的有点丑陋)

下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)

  • 正常访问时

  • 访问次数过于频繁时

自我提问

上述实现就好像就已经达到了我们的接口防刷目的了

但是,还不够

为方便后续描述,项目中新增补充Controller,如下所示

简单来说就是

  • PassCotrollerRefuseController
  • 每个Controller分别有对应的get,post,put,delete类型的方法,其映射路径与方法名称一致

接口自由

  • 对于上述实现,不知道你们有没有发现一个问题
  • 就是现在我们的接口防刷处理,针对是所有的接口(项目案例中我只是写的接口比较少)
  • 而在实际开发中,说对于所有的接口都要做防刷处理,感觉上也不太可能(写此文时目前大四,实际工作经验较少,这里不敢肯定)
  • 那么问题有了,该如何解决呢?目前来说想到两个解决方案
拦截器映射规则

项目通过Git还原到"【Interceptor设置映射规则实现接口自由】"版本即可得到此案例实现

我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可

2.比如说下面的映射配置

3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理

4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:

所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒

  • 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的
  • 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点
  • 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点
  • 这没问题吧
  • 但是现在呢?x, y, z值全都一致了,这就不行了
  • 这就是其中一个不足点
  • 当然,其实针对当前这种情况也有解决方案
  • 那就是弄多个拦截器
  • 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口
  • 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值
  • 这样就是感觉会比较麻烦

防刷接口映射路径修改后维护问题

  • 虽然说防刷接口的映射路径基本上定下来后就不会改变
  • 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)
  • 也就是说还是会有那种要修改接口的映射路径需求
  • 当防刷接口数量特别多,后面的接手人员就很痛苦了
  • 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的
  • 而使用当前这种方式的话,谁维护谁蛋疼
自定义注解 + 反射

咋说呢

  • 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒
  • 自定义注解 + 在需要进行防刷处理的各个接口方法上
  • 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的

下面做个实现

声明自定义注解

Controlller中方法中使用

Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor 
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 锁住时的key前缀
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 统计次数时的key前缀
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
//        自定义注解 + 反射 实现
        // 判断访问的是否是接口方法
        if(handler instanceof HandlerMethod)
            // 访问的是接口方法,转化为待访问的目标方法对象
            HandlerMethod targetMethod = (HandlerMethod) handler;
            // 取出目标方法中的 AccessLimit 注解
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
            // 判断此方法接口是否要进行防刷处理(方法上没有对应注解就代表不需要,不需要的话进行放行)
            if(!Objects.isNull(accessLimit))
                // 需要进行防刷处理,接下来是处理逻辑
                String ip = request.getRemoteAddr();
                String uri = request.getRequestURI();
                String lockKey = LOCK_PREFIX + ip + uri;
                Object isLock = redisTemplate.opsForValue().get(lockKey);
                // 判断此ip用户访问此接口是否已经被禁用
                if (Objects.isNull(isLock)) 
                    // 还未被禁用
                    String countKey = COUNT_PREFIX + ip + uri;
                    Object count = redisTemplate.opsForValue().get(countKey);
                    long second = accessLimit.second();
                    long maxTime = accessLimit.maxTime();

                    if (Objects.isNull(count)) 
                        // 首次访问
                        log.info("首次访问");
                        redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                     else 
                        // 此用户前一点时间就访问过该接口,且频率没超过设置
                        if ((Integer) count < maxTime) 
                            redisTemplate.opsForValue().increment(countKey);
                         else 

                            log.info("禁用访问", ip, uri);
                            long forbiddenTime = accessLimit.forbiddenTime();
                            // 禁用
                            redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                            // 删除统计--已经禁用了就没必要存在了
                            redisTemplate.delete(countKey);
                            throw new CommonException(ResultCode.ACCESS_FREQUENT);
                        
                    
                 else 
                    // 此用户访问此接口已被禁用
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                
            
        
        return  true;
    

由于不好演示效果,这里就不贴测试结果图片了

项目通过Git还原到"【自定义主键+反射实现接口自由"版本即可得到此案例实现,后面自己可以针对接口做下测试看看是否如同我所说的那样实现自定义x, y, z 的效果

嗯,现在看起来,可以针对每个要进行防刷处理的接口进行针对性自定义多长时间内的最大访问次数,以及禁用时长,哪个接口需要,就直接+在那个接口方法出即可

感觉还不错的样子,现在网上挺多资料也都是这样实现的

但是还是可以有改善的地方

先举一个例子,以我们的PassController为例,如下是其实现

下图是其映射路径关系

同一个Controller的所有接口方法映射路径的前缀都包含了/pass

我们在类上通过注解@ReqeustMapping标记映射路径/pass,这样所有的接口方法前缀都包含了/pass,并且以致于后面要修改映射路径前缀时只需改这一块地方即可

这也是我们使用SpringMVC最常见的用法

那么,我们的自定义注解也可不可以这样做呢?先无中生有个需求

假设PassController中所有接口都是要进行防刷处理的,并且他们的x, y, z值就一样

如果我们的自定义注解还是只能加载方法上的话,一个一个接口加,那么无疑这是一种很呆的做法

要改的话,其实也很简单,首先是修改自定义注解,让其可以作用在类上

接着就是修改AccessLimitInterceptor的处理逻辑

AccessLimitInterceptor中代码修改的有点多,主要逻辑如下

与之前实现比较,不同点在于x, y, z的值要首先尝试在目标类中获取

其次,一旦类中标有此注解,即代表此类下所有接口方法都要进行防刷处理

如果其接口方法同样也标有此注解,根据就近优先原则,以接口方法中的注解标明的值为准

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor 
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 锁住时的key前缀
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 统计次数时的key前缀
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 

//      自定义注解 + 反射 实现, 版本 2.0
        if (handler instanceof HandlerMethod) 
            // 访问的是接口方法,转化为待访问的目标方法对象
            HandlerMethod targetMethod = (HandlerMethod) handler;
            // 获取目标接口方法所在类的注解@AccessLimit
            AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
            // 特别注意不能采用下面这条语句来获取,因为 Spring 采用的代理方式来代理目标方法
            //  也就是说targetMethod.getClass()获得是class org.springframework.web.method.HandlerMethod ,而不知我们真正想要的 Controller
//            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);
            // 定义标记位,标记此类是否加了@AccessLimit注解
            boolean isBrushForAllInterface = false;
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            long second = 0L;
            long maxTime = 0L;
            long forbiddenTime = 0L;
            if (!Objects.isNull(targetClassAnnotation)) 
                log.info("目标接口方法所在类上有@AccessLimit注解");
                isBrushForAllInterface = true;
                second = targetClassAnnotation.second();
                maxTime = targetClassAnnotation.maxTime();
                forbiddenTime = targetClassAnnotation.forbiddenTime();
            
            // 取出目标方法中的 AccessLimit 注解
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
            // 判断此方法接口是否要进行防刷处理
            if (!Objects.isNull(accessLimit)) 
                // 需要进行防刷处理,接下来是处理逻辑
                second = accessLimit.second();
                maxTime = accessLimit.maxTime();
                forbiddenTime = accessLimit.forbiddenTime();
                if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) 
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                
             else 
                // 目标接口方法处无@AccessLimit注解,但还要看看其类上是否加了(类上有加,代表针对此类下所有接口方法都要进行防刷处理)
                if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) 
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                
            
        
        return true;
    

    /**
     * 判断某用户访问某接口是否已经被禁用/是否需要禁用
     *
     * @param second        多长时间  单位/秒
     * @param maxTime       最大访问次数
     * @param forbiddenTime 禁用时长 单位/秒
     * @param ip            访问者ip地址
     * @param uri           访问的uri
     * @return ture为需要禁用
     */
    private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) 
        String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        // 判断此ip用户访问此接口是否已经被禁用
        if (Objects.isNull(isLock)) 
            // 还未被禁用
            String countKey = COUNT_PREFIX + ip + uri;
            Object count = redisTemplate.opsForValue().get(countKey);
            if (Objects.isNull(count)) 
                // 首次访问
                log.info("首次访问");
                redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
             else 
                // 此用户前一点时间就访问过该接口,且频率没超过设置
                if ((Integer) count < maxTime) 
                    redisTemplate.opsForValue().increment(countKey);
                 else 
                    log.info("禁用访问", ip, uri);
                    // 禁用
                    redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                    // 删除统计--已经禁用了就没必要存在了
                    redisTemplate.delete(countKey);
                    return true;
                
            
         else 
            // 此用户访问此接口已被禁用
            return true;
        
        return false;
    

好了,这样就达到我们想要的效果了

项目通过Git还原到"【自定义注解+反射实现接口自由-版本2.0】"版本即可得到此案例实现,自己可以测试万一下

这是目前来说比较理想的做法,至于其他做法,暂时没啥了解到

时间逻辑漏洞

这是我一开始都有留意到的问题

也是一直搞不懂,就是我们现在的所有做法其实感觉都不是严格意义上的x秒内y次访问次数

特别注意这个x秒,它是连续,任意的(代表这个x秒时间片段其实是可以发生在任意一个时间轴上)

我下面尝试表达我的意思,但是我不知道能不能表达清楚

假设我们固定某个接口5秒内只能访问3次,以下面例子为例

底下的小圆圈代表此刻请求访问接口

按照我们之前所有做法的逻辑走

  1. 第2秒请求到,为首次访问,Redis中统计次数为1(过期时间为5秒)
  2. 第7秒,此时有两个动作,一是请求到,二是刚刚第二秒Redis存的值现在过期
  3. 我们先假设这一刻,请求处理完后,Redis存的值才过期
  4. 按照这样的逻辑走
  5. 第七秒请求到,Redis存在对应key,且不大于3, 次数+1
  6. 接着这个key立马过期
  7. 再继续往后走,第8秒又当做新的一个起始,就不往下说了,反正就是不会出现禁用的情况

按照上述逻辑走,实际上也就是说当出现首次访问时,当做这5秒时间片段的起始

第2秒是,第8秒也是

但是有没有想过,实际上这个5秒时间片段实际上是可以放置在时间轴上任意区域的

上述情况我们是根据请求的到来情况人为的把它放在【2-7】,【8-13】上

而实际上这5秒时间片段是可以放在任意区域的

那么,这样的话,【7-12】也可以放置

而【7-12】这段时间有4次请求,就达到了我们禁用的条件了

是不是感觉怪怪的

想过其他做法,但是好像严格意义上真的做不到我所说的那样(至少目前来说想不到)

之前我们的做法,正常来说也够用,至少说有达到防刷的作用

后面有机会的话再看看,不知道我是不是钻牛角尖了

路径参数问题

假设现在PassController中有如下接口方法

也就是我们在接口方法中常用的在请求路径中获取参数的套路

但是使用路径参数的话,就会发生问题

那就是同一个ip地址访问此接口时,我携带的参数值不同

按照我们之前那种前缀+ip+uri拼接的形式作为key的话,其实是区分不了的

下图是访问此接口,携带不同参数值时获取的uri状况

这样的话在我们之前拦截器的处理逻辑中,会认为是此ip用户访问的是不同的接口方法,而实际上访问的是同一个接口方法

也就导致了【接口防刷】失效

接下来就是解决它,目前来说有两种

  1. 不要使用路径参数

这算是比较理想的做法,相当于没这个问题

但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数

  1. 替换uri
  • 我们获取uri的目的,其实就是为了区别访问接口
  • 而把uri替换成另一种可以区分访问接口方法的标识即可
  • 最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可
  • 当然,其实不同的Controller中,其接口方法名称也有可能是相同的
  • 实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可
  • 实际解决方案有很多,看个人需求吧

真实ip获取

在之前的代码中,我们获取代码都是通过request.getRemoteAddr()获取的

但是后续有了解到,如果说通过代理软件方式访问的话,这样是获取不到来访者的真实ip的

至于如何获取,后续我再研究下http再说,这里先提个醒

总结

说实话,挺有意思的,一开始自己想【接口防刷】的时候,感觉也就是转化成统计下访问次数的问题摆了。后面到网上看别人的写法,又再自己给自己找点问题出来,后面会衍生出来一推东西出来,诸如自定义注解+反射这种实现方式。

以前其实对注解 + 反射其实有点不太懂干嘛用的,而从之前的数据报表导出,再到基本权限控制实现,最后到今天的【接口防刷】一点点来进步去补充自己的知识点,而且,感觉写博客真的是件挺有意义的事情,它会让你去更深入的了解某个点,并且知识是相关联的,探索的过程中会牵扯到其他别的知识点,就像之前的写的【单例模式】实现,一开始就了解到懒汉式,饿汉式

后面深入的话就知道其实会还有序列化/反序列化,反射调用生成实例,对象克隆这几种方式回去破坏单例模式,又是如何解决的,这也是一个进步的点,后续为了保证线程安全问题,牵扯到的synchronized,voliate关键字,继而又关联到JVM,JUC,操作系统的东西。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2022最新版)

2.劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

后台登陆防刷、防爆破以及正常的登录校验

参考技术A 前几天项目上需要对一个正常登陆接口,以及忘记密码的接口进行防爆破处理,这里我用nginx,redis,以及前端的一些简单的图形拖动来做一个简单的安全机制,可能有不完善的地方,大家可以提出来意见。

其实一个接口是无法完全避免接口爆破的,区分人和机器或许可以使用谷歌的图片验证机制,但是我们一般简单项目没必要做那么复杂的,只需要确保不正常的访问频率不会爆破出我们的用户信息,以及让我们机器的处理流量保存在可控范围即可。

验证码只能60s获取一次 并且3小时内只能获取三次,超过次数提升获取频繁,稍后再试。

正常登录1小时内失败6次账号自动锁定,1小时之后自动解锁。

获取验证码无论输入的账号存在不存在均显示发送成功,但是实际不存在的账号不会正常发送。

4.登录失败,账号不存在密码错误不再提示账号不存在等等,而是统一显示账号或密码错误。5.忘记密码前端部分增加滑动校验,60倒计时无法点击发送验证码。前后端共同校验。6.技术限制系统此接口的访问频率。

前端部分可以在这个地址看看这几个简单的组件,这次我们就使用最简单的,滑动拖动即可。

<drag-verify

              ref="dragVerify"

              :width="width"

              :height="height"

              text="请按住滑块拖动"

              successText="验证通过"

              :isPassing.sync="isPassing"

              background="#ccc"

              completedBg="rgb(105, 231, 251)"

              handlerIcon="el-icon-d-arrow-right"

              successIcon="el-icon-circle-check"

              @passcallback="passcallback"

          >

          </drag-verify>

用户滑动之后需要加上60s倒计时,这块我们使用定时器实现即可,以及邮箱和手机号的正确性校验,不正确则弹窗提示。

this.countDown = 60;

      timer = setInterval(() =>

        if (this.countDown - 1 >= 0)

          this.countDown -= 1;

        else

          clearInterval(timer);

          timer = null;

       

      , 1000);

<el-button disabled type="text" v-show="time > 0">

time > 0 ? `$time` : "" s之后重试</el-button>

验证邮箱手机号可以使用正则校验进行。

mobileReg = /^1\d10$/;

      emailReg = /^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]2,8)$/;

前端大体思路就是,进行滑块验证,拖到右边之后,60s之内无法操作,60s到期之后自动复原,

显示倒计时时间。这个只能防止用户在页面上多次点击,造成一个验证的假象,如果直接对后端接口爆破,则无法避免。

这是大概的流程图,图中还有些细节问题下面慢慢讲解。

这块本来我想用java或者kotlin写,但是历史项目用go写的,重写的话还有其他一些改动,所以继续使用golang完成这部分逻辑。

先定义一个结构体,然后我们来分析下需要哪些字段来实现我们的业务。

type CommonLogin struct

    CreateTime time.Time

    LastTime  time.Time

    Times      uint8



// 登录的前置校验

func beforeCommonLoginValid(key string, r *redis.Client, field string) (bool, error)

    // redis中是否存在账号

    result, err := r.HExists(field, key).Result()

    if err != nil

            fmt.Printf("从redis中获取用户账户失败,账户为: %s", key)

            return false, err

   

    if result

            login := &CommonLogin

            // 存在账号 说明之前登录失败过 且自从上次失败未登录成功过

            commonLogin, err := r.HGet(field, key).Result()

            if err != nil

                    return false, err

           

            json.Unmarshal([]byte(commonLogin), login)

            if login.Times < 6

                    return true, nil

           

            // 是否在1小时内失败了6次

            if login.Times >= 6

                    // 否

                    if time.Now().Sub(login.CreateTime) > time.Hour*1

                            // 连续输错6次时长大于1小时 解锁

                            r.HDel(field, key)

                            return true, nil

                    else

                            fmt.Printf("用户%s于1小时之内连续登录失败6次,账号锁定,1小时后重试。", key)

                            return false, nil

                   

           

   

    // redis中不存在重试记录

    return true, nil



在所有的登录判断的出口,调用此方法即可,例如用户名密码错误,acl校验未通过等等。

其实原理差不多,唯一的区别就是多了一个获取验证码时间间隔校验。

func beforeForgotPasswordValid(key string, r *redis.Client, field string) (bool, error)

    // redis中是否存在账号

    result, err := r.HExists(field, key).Result()

    if err != nil

            fmt.Printf("从redis中获取用户账户失败,账户为: %s", key)

            return false, err

   

    login := &CommonLogin

    // 账号存在

    if result

            commonLogin, err := r.HGet(field, key).Result()

            if err != nil

                    return false, err

           

            json.Unmarshal([]byte(commonLogin), login)

            // 获取验证码间隔时长不能小于60s

            if time.Now().Sub(login.LastTime) < time.Second*60

                    fmt.Printf("用户获取验证码间隔小于60s")

                    return false, nil

           

            if login.Times < 3

                    return true, nil

           

            // 是否在1小时内获取了3次

            if login.Times >= 3

                    // 否

                    if time.Now().Sub(login.CreateTime) > time.Hour*3

                            // 连续输错6次时长大于1小时 解锁

                            r.HDel(field, key)

                            return true, nil

                    else

                            fmt.Printf("用户%s于3小时之内连续获取验证码3次,账号锁定,3小时后重试。", key)

                            return false, nil

                   

           

   

    return true, nil



// 更新获取验证码的时间

func afterForgotPasswordValid(key string, r *redis.Client, field string)

    login := &CommonLogin

    commonLogin, _ := r.HGet(field, key).Result()

    json.Unmarshal([]byte(commonLogin), login)

    // 验证码发送成功

    result, _ := r.HExists(field, key).Result()

    if result

            login.Times = login.Times + 1

            login.LastTime = time.Now()

            data, _ := json.Marshal(login)

            r.HSet(field, key, data)

    else

            login.Times = 1

            login.LastTime = time.Now()

            login.CreateTime = login.LastTime

            data, _ := json.Marshal(login)

            r.HSet(field, key, data)

   



nginx是一个非常强大的中间价,在安全方面,我们可以用它来限制来自于同一机器的访问频率,可以做黑名单功能等等,当然有人会说ip代过滤理池之类的,我们此次演示的只是简单demo,恶意攻击当然需要专业防护了。

具体google一下,看这两篇官方文档。

https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http

https://www.nginx.com/blog/rate-limiting-nginx/

具体的配置其实很简单了。

限制远程同ip访问频率。

limit_req_zone$binary_remote_addrzone=perip:10mrate=1r/s;

$binary_remote_addr 表示通过remote

addr这个标识来做限制,“binary ”的目的是缩写内存占用量,是限制同一客户端ip地址

zone=one:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息

rate=1r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒1次,还可以有比如30r/m的

location ^~ /api/xxx

        limit_req zone=perip nodelay;

        limit_req_status 503;

        proxy_pass http://正确地址;

   

上面配置意思就是超过频率返回503,服务不可用。

使用jmeter进行压力测试:1s 10个请求,我们预期只有1个请求成功,其他的返回503.

以上是关于接口防刷处理方案,太优雅了!的主要内容,如果未能解决你的问题,请参考以下文章

后台登陆防刷、防爆破以及正常的登录校验

牛X!一个注解搞定接口防刷!

SpringBoot一个注解,实现接口防刷

SpringBoot一个注解,实现接口防刷

通过注解实现接口限流防刷

一个注解搞定 SpringBoot 接口防刷,还有谁不会?