😱什么?刚买的10W条短信被盗刷完了?!

前言

可乐他们团队最近在做一个文章社区平台,由于人手不够,后端部分也是由前端同学开发,用的框架是 nest.js

他们平台上线之后,注册的用户量日渐增长,老板十分开心。

由于注册的时候需要发送短信验证码来校验用户身份的真实性,所以老板一口气又去买了 10W 条短信,幻想着哪一天到达 10W 用户。

好景不长的是,刚买了没多久,就收到了短信平台的告警,说的是短信快用完了?

老板又震惊又愤怒,找到可乐:怎么这么快就用完了?我看也没几个新用户注册啊?

可乐看了一下:老板,我们发短信验证码的接口只是做了一层前端的验证校验,没有做其他的任何限制,抓个包就能调用发送,我们的短信被盗刷了!

老板:😠,那你还不赶紧修复一下,不然我后续买多少条短信都不够用的。

PS:在本文行文中,以发送邮件验证码来替代发送短信验证码。

前端校验

前端的验证码校验用的是rc-slider-captcha这个库,这是一个滑块验证码相关的前端库。

它从交互上支持两种形式的滑块验证,一种是滑动缺口图去完成拼图

另一种是纯轨迹滑动,没有拼图

这个库的作者还很贴心的做了一个客户端的拼图生成器,结合用起来确实非常舒服。

我这边使用的是纯轨迹滑动的接入方式,实现起来也很简单,当点击发送验证码的时候会弹出弹窗,然后让用户进行滑块验证。

通过滑动的轨迹判断用户验证是否通过,如果通过就执行发送验证码的逻辑。

js 复制代码
      <Modal
        open={visible}
        onCancel={() => setVisible(false)}
        title="安全验证"
        destroyOnClose
        footer={false}
        centered
        width={368}
        style={{ maxWidth: "100%" }}
      >
        <SliderCaptcha
          autoRefreshOnError={true}
          mode="slider"
          onVerify={(data) => {
            if (data.x >= 260) {
              setTimeout(() => {
                setVisible(false);
                sendPhoneCode(); // 发送验证码
              });
              return Promise.resolve();
            }
            return Promise.reject();
          }}
        />
      </Modal>

但是呢,这种做法只能说是掩耳盗铃,别人一抓包找到你真正发送短信验证码的接口就可以开始搞事情了。本质上的原因是我们发送验证码的时候,没有校验用户是否通过了前端的滑块验证。

防接口盗刷

这个时候我们就需要一种前后端一起验证的机制,我这里使用的是Google reCAPTCHA

你当然也可以使用别的第三方的服务,比如下面的:

在这里注册好一个网站

注册好之后会有一对密钥对,我们需要自行记录一下

然后就可以在我们的网站中接入这个谷歌验证了:

可以通过下面这个script引入验证码组件:

js 复制代码
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>

然后写一个验证码容器 div

js 复制代码
<div id="robot"></div>

然后在点击发送验证码的时候唤起谷歌验证组件:

js 复制代码
  const handleClick = async () => {
    const res = await form.validateFields(["email"]);
    try {
      window.grecaptcha.reset();
    } catch (error) {}
    window.grecaptcha.render("robot", {
      sitekey: "你的sitekey", //公钥
      callback: function (code: string) {
        doSend(res.email, code);
      },
      "expired-callback": () => {
        message.error("验证过期");
      },
      "error-callback": () => {
        message.error("验证错误");
      },
    });
  };

在验证通过之后,我们就把用户输入的邮箱以及谷歌验证码组件的返回值一起发送给后端。后端去调用谷歌验证的接口,来判断用户的验证是否通过。如果通过,则走发送验证码(我这里使用发送邮件验证码的服务来替代短信验证码,大家懂这个意思就行)的逻辑:

js 复制代码
  async sendVerifyCode(
    email: string,
    googleCode: string,
    remoteip: string,
  ): Promise<string> {
    if (!googleCode) {
      throw new Error('验证code不可为空');
    }
    const res = await axios.post(
      'https://www.google.com/recaptcha/api/siteverify',
      {
        secret: this.siteKey, // 私钥
        response: googleCode,
      },
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      },
    );
    if (!res.data.success) {
      throw Error('非法验证');
    }
    const code = generateRandomNumber();
    const text = `您的验证码是:${code},5分钟内有效`;
    await this.emailService.sendMail(email, 'jueyin注册', text);
    await this.redisService.set(`${VERIFY_CODE_PREFIX}:${email}`, code, 5 * 60);
    return code;
  }

按钮倒计时

一般在发送完验证码之后,会有一个倒计时的流程,不会让用户马上又能重新发送验证码

这里可以用到 redis 进行一个限流,在 redis 中设置一个 60s 过期的 key 。当请求过来时,检查一下这个 key 是否还存在,如果存在,则抛出异常,如果不存在则设置这个 key ,并发送验证码。

js 复制代码
    const cache = await this.redisService.get(`code::cache::${email}`);
    if (cache) {
      throw Error('请稍后再发送验证码');
    }
    await this.redisService.set(`code::cache::${email}`, true, 60);

账号限频

同样的,我们可以通过 redis 来对发送的账号进行限制频率,比如说我只让一个邮箱或者手机号每天只能发送 5 条验证码。

我可以这么写:

js 复制代码
  private async refreshCount(email: string) {
    const redisClient = this.redisService.getClient();
    const key = `code::count::${email}`;
    let countRes = await redisClient.get(key);
    const exist = await redisClient.exists(key);
    const count = Number(countRes);
    if (exist && count > 5) {
      throw Error('请求太过频繁,请稍后再试');
    }
    if (exist) {
      const ttl = await redisClient.ttl(key);
      await redisClient.set(key, count + 1);
      await redisClient.expire(key, ttl);
    } else {
      await redisClient.setex(key, 1, 60 * 60 * 12); // 一天
    }
  }
  1. 在用户第一次请求时,初始化 rediskey 的值为 1 ,并设置过期时间为一天
  2. 后续的请求时,增加这个 key 的值,如果超过了阈值,则抛异常

最后

以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~

相关推荐
f8979070701 小时前
layui动态表格出现 横竖间隔线
前端·javascript·layui
二十雨辰1 小时前
[uni-app]小兔鲜-04推荐+分类+详情
前端·javascript·uni-app
霸王蟹2 小时前
Vue3 项目中为啥不需要根标签了?
前端·javascript·vue.js·笔记·学习
lucifer3113 小时前
深入解析 React 组件封装 —— 从业务需求到性能优化
前端·react.js
儒雅的烤地瓜3 小时前
JS | 如何解决ajax无法后退的问题?
前端·javascript·ajax·pushstate·popstate事件·replacestate
觉醒法师3 小时前
Vue3+TS项目 - ref和useTemplateRef获取组件实例
开发语言·前端·javascript
老章学编程i3 小时前
Vue工程化开发
开发语言·前端·javascript·vue.js·前端框架
什么鬼昵称4 小时前
Pikachu-PHP反序列化
开发语言·javascript·php
果子切克now4 小时前
vue3导入本地图片2种实现方法
前端·javascript·vue.js
谢尔登5 小时前
【移动端】事件基础
前端·javascript·html