吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3807|回复: 11
收起左侧

[其他原创] 【原创】后台登陆防刷、防爆破以及正常的登录校验

[复制链接]
dididada 发表于 2021-12-31 11:06
本帖最后由 dididada 于 2022-1-6 10:26 编辑

背景

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

技术分析

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

  1. 验证码只能60s获取一次 并且3小时内只能获取三次,超过次数提升获取频繁,稍后再试。
  2. 正常登录1小时内失败6次账号自动锁定,1小时之后自动解锁。
  3. 获取验证码无论输入的账号存在不存在均显示发送成功,但是实际不存在的账号不会正常发送。
    4.登录失败,账号不存在密码错误不再提示账号不存在等等,而是统一显示账号或密码错误。
    5.忘记密码前端部分增加滑动校验,60倒计时无法点击发送验证码。前后端共同校验。
    6.技术限制系统此接口的访问频率。

前端部分

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

          <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\d{10}$/;
      emailReg = /^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,8})$/;

前端大体思路就是,进行滑块验证,拖到右边之后,60s之内无法操作,60s到期之后自动复原,
显示倒计时时间。这个只能防止用户在页面上多次点击,造成一个验证的假象,如果直接对后端接口爆破,则无法避免。

后端


这是大概的流程图,图中还有些细节问题下面慢慢讲解。
sc_20211202164056.png
这块本来我想用java或者kotlin写,但是历史项目用go写的,重写的话还有其他一些改动,所以继续使用golang完成这部分逻辑。

先定义一个结构体,然后我们来分析下需要哪些字段来实现我们的业务。
我们需要一个首次登陆时间,最后一次登陆时间,和总的登录次数足以判断:

  1. 验证码间隔
  2. 登陆失败次数
  3. 距离第一次登陆失败次数间隔

    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
    }

    此方法相当于一个切面,在进入登录逻辑之前调用即可。与此同时没有一个后置校验,来处理登录之后的成功或失败,成功则清空登录信息,失败则累加失败次数,以此来锁定账号,或者长时间登录之后解锁账号。

    正常登陆的后置校验
    // 登录的后置校验
    func afterCommonLoginValid(key string, r *redis.Client, loginState bool) {
        field := engine.Config().GetString("common_login")
        result, _ := r.HExists(field, key).Result()
        login := &CommonLogin{}
        // 存在
        if result {
                // 登录失败
                if !loginState {
                        // 更新登录失败次数和登录失败的时间
                        commonLogin, _ := r.HGet(field, key).Result()
                        json.Unmarshal([]byte(commonLogin), login)
                        login.Times = login.Times + 1
                        login.LastTime = time.Now()
                        data, _ := json.Marshal(login)
                        r.HSet(field, key, data)
                } else {
                        r.HDel(field, key)
                }
                // 不存在
        } else {
                // 且首次登录失败
                if !loginState {
                        login.Times = 1
                        login.LastTime = time.Now()
                        login.CreateTime = login.LastTime
                        data, _ := json.Marshal(login)
                        r.HSet(field, key, data).Result()
                }
        }
    }

    在所有的登录判断的出口,调用此方法即可,例如用户名密码错误,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进行接口访问频率限制

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_addr zone=perip:10m rate=1r/s;

解释下这段配置的参数:
$binary_remote_addr 表示通过remoteaddr这个标识来做限制,“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.


sc_20211208163924.png
核心逻辑其实就是上面这些,源吗就不贴出来了,有不懂的再讨论吧。

免费评分

参与人数 3吾爱币 +9 热心值 +3 收起 理由
ZZF1949 + 1 + 1 谢谢@Thanks!
啦啦啦听不见 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| dididada 发表于 2021-12-31 12:30
冥界3大法王 发表于 2021-12-31 12:27
我觉得得从数量上提高强度。假设有1万处暗桩,还有人尝试破解吗?

什么数量?暗桩是什么意思,现在这个方案其实足够大多数场景了。还有一些专用的授权服务器, 间隔刷新验证码,基本上登陆都是这种认证 + 基本身份验证。
arialyy 发表于 2021-12-31 11:58
 楼主| dididada 发表于 2021-12-31 12:26
冥界3大法王 发表于 2021-12-31 12:27
我觉得得从数量上提高强度。假设有1万处暗桩,还有人尝试破解吗?
dork 发表于 2021-12-31 15:55
OK借鉴参考一下
 楼主| dididada 发表于 2021-12-31 16:04
dork 发表于 2021-12-31 15:55
OK借鉴参考一下

实现基本验证完全没问题
xhg2992 发表于 2021-12-31 16:56
很多应用大体也是这个思路了,学习了
头像被屏蔽
heng179 发表于 2021-12-31 19:05
提示: 作者被禁止或删除 内容自动屏蔽
XU19951206 发表于 2021-12-31 19:18
学习一下,感谢卤煮
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-25 09:44

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表