最近接到一个新的需求,忘记密码的界面只有手机号,图片验证码以及手机验证码,这就导致了手机验证码可能被人暴力破解,导致密码被修改
项目是以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")