游戏开发工具

Redis限速器实例

为了保障系统的安全性和性能,并保证系统的重要资源不被滥用,应用程序常常会对用户的某些行为进行限制,比如:


为了防止网站内容被网络爬虫抓取,网站管理者通常会限制每个IP地址在固定时间段内能够访问的页面数量,比如1min之内最多只能访问30个页面,超过这一限制的用户将被要求进行身份验证,确认本人并非网络爬虫,或者等到限制解除之后再进行访问。


为了防止用户的账号遭到暴力破解,网上银行通常会对访客的密码试错次数进行限制,如果一个访客在尝试登录某个账号的过程中,连续好几次输入了错误的密码,那么这个账号将被冻结,只能等到第二天再尝试登录,有的银行还会向账号持有者的手机发送通知来汇报这一情况。


实现这些限制机制的其中一种方法是使用限速器,它可以限制用户在指定时间段之内能够执行某项操作的次数。


代码清单2-8展示了一个使用字符串键实现的限速器,这个限速器程序会把操作的最大可执行次数存储在一个字符串键里面,然后在用户每次尝试执行被限制的操作之前,使用DECR命令将操作的可执行次数减1,最后通过检查可执行次数的值来判断是否执行该操作。


代码清单2-8 倒计时式的限速器:/string/limiter.py

class Limiter:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def set_max_execute_times(self, max_execute_times):
        """
        设置操作的最大可执行次数
        """
        self.client.set(self.key, max_execute_times)

    def still_valid_to_execute(self):
        """
        检查是否可以继续执行被限制的操作,是则返回True,否则返回False
        """
        num = self.client.decr(self.key)
        return (num >= 0)

    def remaining_execute_times(self):
        """
        返回操作的剩余可执行次数
        """
        num = int(self.client.get(self.key))
        if num < 0:
            return 0
        else:
            return num


这个限速器的关键在于set_max_execute_times()方法和still_valid_to_execute()方法:前者用于将最大可执行次数存储在一个字符串键里面,后者则会在每次被调用时对可执行次数执行减1操作,并检查目前剩余的可执行次数是否已经变为负数,如果为负数,则表示可执行次数已经耗尽,不为负数则表示操作可以继续执行。


以下代码展示了这个限制器的使用方法:

>>> from redis import Redis
>>> from limiter import Limiter
>>> client = Redis(decode_responses=True)
>>> limiter = Limiter(client, 'wrong_password_limiter') # 密码错误限制器
>>> limiter.set_max_execute_times(3)                    # 最多只能输入3次错误密码
>>> limiter.still_valid_to_execute()  # 前3次操作能够顺利执行
True
>>> limiter.still_valid_to_execute()
True
>>> limiter.still_valid_to_execute()
True
>>> limiter.still_valid_to_execute()  # 从第4次开始,操作将被拒绝执行
False
>>> limiter.still_valid_to_execute()
False


以下伪代码则展示了如何使用这个限速器去限制密码的错误次数:

# 试错次数未超过限制
while limiter.still_valid_to_execute():
    # 获取访客输入的账号和密码
    account, password = get_user_input_account_and_password()
    # 验证账号和密码是否匹配
    if password_match(account, password):
        ui_print("密码验证成功")
    else:
        ui_print("密码验证失败,请重新输入")
# 试错次数已超过限制
else:
    # 锁定账号
    lock_account(account)
    ui_print("连续尝试登录失败,账号已被锁定,请明天再来尝试登录。")