最近接到一个新的需求,忘记密码的界面只有手机号,图片验证码以及手机验证码,这就导致了手机验证码可能被人暴力破解,导致密码被修改

项目是以Python Flask框架搭建

一、分析需求

防止暴力破解的核心在于防止用户低成本的对手机验证码进行遍历

于是,增加输错验证码的次数就变得至关重要,当用户的手机验证码输入错一定的次数之后,将该手机号冻结一段时间,可以在一定程度上防止爆破

二、代码实现

在原始的接口部分增加错误次数的校验

将其放在校验正确性之前,可以使其即使输入正确的也无法被校验,更符合冻结手机号的设定

@bp.route('/forget_passwd', methods=['POST'])
def interface__forget_passwd():
    try:
        data = request.get_json() or {}
        data = {k: v for k, v in filter(lambda x: x[1] or x[1] == 0, data.items())}
        logging.info(f'interface__forget_passwd params: {data}')
     
        required_params = ['username', 'code']
        if not all(map(lambda x: x in data, required_params)):
            logging.error('interface__forget_passwd, 缺少参数')
            return response_with(40000, '缺少参数')

        if not db.session.query(models.User).filter_by(
                username=data['username']).all():
            return response_with(40000, '用户名尚未注册')

        if check_attempts(data['username']):
            logging.error('interface__forget_passwd, 验证码错误次数过多')
            return response_with(40000, '验证码错误次数过多,请两分钟后再尝试')

        check_result = check_phone_verify_code(data['username'], data['code'])
        if not check_result:
            logging.error('interface__forget_passwd, 请输入正确的验证码')
            return response_with(40000, '请输入正确的验证码')
        return response_with(20000, '验证码正确')
    except Exception as e:
        logging.exception(e)
        return response_with(50000)

检查手机号是否达到最大验证码请求次数

从Redis中获取该手机号的尝试次数

def check_max_attempts(phone_number):
    attempts_number_str = redis_client.get(f'verify_attempts:{phone_number}')

    try:
        attempts_number = int(attempts_number_str) if attempts_number_str is not None else 0
    except (ValueError, TypeError) as e:
        logging.error(f"手机号 {phone_number} 的尝试次数无效: {attempts_number_str}, 错误: {e}")
        return False

    logging.info(f"手机号 {phone_number} 的验证码错误次数: {attempts_number}")

    if attempts_number >= MAX_VERIFY_ATTEMPTS:
        logging.warning(f"手机号 {phone_number} 达到最大验证码错误次数")
        return True
    return False

检查手机验证码是否正确

若手机验证码正确则调用add_phone_verify_code_number(phone_number) 函数,对Redis中的数据进行更新

def check_phone_verify_code(phone_number, verify_code):
    phone_result = check_phone_number(phone_number)
    if not phone_result:
        logging.error(f"手机号 {phone_number} 无效")
        return False

    hash_verify_code = hash_get(phone_number, "code")
    if hash_verify_code is None:
        logging.error(f"未找到手机号 {phone_number} 的验证码")
        return False

    if verify_code == hash_verify_code:
        reset_attempts(phone_number)
        return True

    add_phone_verify_code_number(phone_number)
    return False

增加手机号的验证码请求计数

首先从Redis中获取当前的尝试次数再更新

当尝试次数到达上限之后,设置冷却时间

def add_phone_verify_code_number(phone_number):
    attempts_number = get_or_init_attempts(phone_number)
    attempts_number += 1
    redis_client.set(f'verify_attempts:{phone_number}', str(attempts_number))

    logging.info(f"手机号 {phone_number} 的验证码请求次数更新为: {attempts_number}")

    if attempts_number >= MAX_VERIFY_ATTEMPTS:
        redis_client.expire(f'verify_attempts:{phone_number}', COOLDOWN_DURATION)
        logging.warning(
            f"手机号 {phone_number} 达到最大验证码错误次数,设置冷却时间到: {time.ctime(time.time() + COOLDOWN_DURATION)}")

获取或初始化手机号的验证码请求计数

def get_or_init_attempts(phone_number):
    attempts_number_str = redis_client.get(f'verify_attempts:{phone_number}')
    if attempts_number_str is None:
        return 0
    try:
        return int(attempts_number_str)
    except (ValueError, TypeError) as e:
        logging.error(f"手机号 {phone_number} 的尝试次数无效: {attempts_number_str}, 错误: {e}")
        return 0

当验证码正确或过期后重置尝试次数

def reset_attempts(phone_number):
    redis_client.set(f'verify_attempts:{phone_number}', "0")
    logging.info(f"手机号 {phone_number} 的验证码尝试次数已重置为 0")