爆破原理

“爆破”即“用粗暴的手段进行破解”。

防范方案

防范这一类的问题,基本思路就是限制可验证的次数(限制入侵者可以尝试枚举到达目的的次数)。

限制次数(短期限内)

思路

假设某个类型的验证码(忘记密码或验证登录)有效期为 x 分钟。
我们设置同一个被验证手机号的某个类型验证操作 y 秒 只能被验证 z 次,如果 y 秒内被验证的次数超过 z 次,便终端操作返回错误。
这样的话在 x 分钟内,该手机号获取到的验证码只有 y*z 次“猜”的机会,“猜”中几率降得非常低。

实现

首先,我们可以借助 redis 缓存数据库 存储手机号在某段时间内的验证次数:

  1. 设定键名:针对某个手机号的某种验证行为(如 “150083xxxxx” 的手机号进行 “忘记密码”的操作)设立一个key。
  2. 设有效期:针对刚才设定的 key 设定有效期规则(如 1分钟只能被验证 3 次,正常用户操作不会输入错误 3 次)。

接着来编写我们的业务逻辑代码:

  1. 从 redis 中查询记录某个手机号的验证行为 key 是否存在
    • 如果 key 不存在,创建该 key,并记录值为1,同时设定 key 的有效期
    • 如果 key 存在,判断 key 记录的值是否已经达到上限,若达到上限查询剩余有效期并返回错误与提示
  2. 最后,操作 key 自增1,增加段时间内的已尝试验证过的次数

    示例

    1. async validateCheckTimes(mobile, action, expireSeconds = 120, limitTimes = 5) {
    2. const redis = this.app.redis;
    3. const key = `verify/checkTimes/${action}/${mobile}`;
    4. const checkTimes = await redis.get(key);
    5. if (!checkTimes) {
    6. await redis.set(key, 1);
    7. await redis.expire(key, expireSeconds);
    8. } else {
    9. if (checkTimes > limitTimes) {
    10. const retrySeconds = await redis.ttl(key);
    11. return {
    12. status: 'error',
    13. text: `操作速度过快,请${retrySeconds}秒后稍后再试!`,
    14. };
    15. }
    16. await redis.incr(key);
    17. }
    18. return {
    19. status: 'normal',
    20. text: '操作正常',
    21. };
    22. }

    锁定帐户(长期限内)

    思路

    锁定帐户的操作有点像我们讲到的第一个方案的加强版,不光限制某段时间内能够尝试验证的次数,还设定了一段时间封锁期。
    假设某项验证操作,我们认为一个正常用户在某一段时间内只会操作 n 次,超过 n 次就可以被判定为是入侵者的恶意尝试行为。
    为了保证该帐户的安全,我们设定 m 分钟的锁定期,在之后的 m 分钟内都不能再进行同样的验证操作。

    实现

    我们完全可以在“限制次数”的实现中进行扩充后面的业务逻辑,能发挥出更大的效用:
    同样地,借助 redis 缓存数据库,存储手机号在 某段短期时间内某段长时间内 的验证次数,以及 帐户锁 的键名:

  3. 设定键名:为三个类型的存储数据设定键名。

  4. 设有效期:针对刚才设定的 key 设定有效期规则,注意区分 某段短时间 和 某段长时间 的时间间隔。

接着来编写我们的业务逻辑代码:

  1. 从 redis 中查询某个手机号的 账户锁 是否存在,存在则返回提示与剩余解锁时间
  2. 从 redis 中查询记录某个手机号的验证行为的 长期 key 是否存在
    • 如果长期 key 不存在,创建该 key,并记录值为1,同时设定 key 的有效期
    • 如果长期 key 存在,判断 key 记录的值是否已经达到上限,若达到上限查询剩余有效期并返回错误与提示
      • 最后,操作长期 key 自增1,增加段时间内的已尝试验证过的次数
  3. 从 redis 中查询记录某个手机号的验证行为的 短期 key 是否存在

    • 如果短期 key 不存在,创建该 key,并记录值为1,同时设定 key 的有效期
    • 如果短期 key 存在,判断 key 记录的值是否已经达到上限,若达到上限查询剩余有效期并返回错误与提示

      • 最后,操作短期 key 自增1,增加段时间内的已尝试验证过的次数

        示例

        ```javascript async validateCheckTimesWithLocker(config) { const { mobile, action, shortExpireSeconds = 60, shortLimitTimes = 3, longExpireMinutes = 30, longLimitTimes = 8, lockMinutes = 30, } = config;

      const redis = this.app.redis; const key = { shortPeriodTimes: verify/checkTimes/${action}/longPeriod/${mobile}, longPeriodTimes: verify/checkTimes/${action}/shortPeriod/${mobile}, locker: verify/locker/${mobile}, };

      const locker = await redis.get(key.locker); if (locker) { const retryMinutes = Math.round((await redis.ttl(key.locker) / 60)); return { status: ‘error’, text: 一段时间内操作次数达上限,账号关键操作已锁定,${retryMinutes}分钟后解锁!, }; }

      const longPeriodCheckTimes = await redis.get(key.longPeriodTimes); if (!longPeriodCheckTimes) { await redis.set(key.longPeriodTimes, 1); await redis.expire(key.longPeriodTimes, longExpireMinutes 60); } else { if (longPeriodCheckTimes > longLimitTimes) { await redis.set(key.locker, 1); await redis.expire(key.locker, lockMinutes 60); const retryMinutes = Math.round((await redis.ttl(key.locker) / 60)); return {

      1. status: 'error',
      2. text: `操作已达上限,账号关键操作已锁定,${retryMinutes}分钟后解锁!`,

      }; } await redis.incr(key.longPeriodTimes); }

      const shortPeriodCheckTimes = await redis.get(key.shortPeriodTimes); if (!shortPeriodCheckTimes) { await redis.set(key.shortPeriodTimes, 1); await redis.expire(key.shortPeriodTimes, shortExpireSeconds); } else { if (shortPeriodCheckTimes > shortLimitTimes) { const retrySeconds = await redis.ttl(key.shortPeriodTimes); return {

      1. status: 'error',
      2. text: `操作速度过快,请${retrySeconds}秒后稍后再试!`,

      }; } await redis.incr(key.shortPeriodTimes); }

      return { status: ‘normal’, text: ‘当前处于验证正常状态’, }; } ``` image.png

      设验证码

      另一种常见的方法就是设验证码,但基础的验证码很容易被AI识别出来,过于复杂的验证码又大大降低了用户体验。
      因此,选择和设计一种合适的验证码或者用专业的 第三方无感验证码识别(比如“腾讯防水墙”、“极验”)是比较好的选择。