Python 生成验证码

2017/7/10 posted in  Python 黑魔法

在 InfoSub 接入验证码的时候,第一次尝试使用验证码,设计的比较简单,利用的是 captchaitsdangerous 模块。

在创建验证码的时候,后台随机生成一个字符串作为验证码(captcha_code),然后利用 itsdangerous.URLSafeTimedSerializer 对这个字符串进行编码,生成一个安全的(不能被随意反向编码,反向编码需要秘钥),方便的(可以直接扔到 url 里)的字符串。把这个字符串作为验证码的 id(captcha_id)。

这样使得校验验证码非常简单,利用秘钥对 captcha_id 反向编码便可以得到 captcha_code,将 captcha_code 与用户的输入进行比对便可以知道用户的验证码是否正确。

配置

首先要配置用于编码的 serializer:

serializer = URLSafeTimedSerializer(SECRET_KEY)

这样便可以使用 serializerloadsdumps 用于编码和反向编码:

captcha_id = serializer.dumps(captcha_code)
captcha_code = serializer.loads(captcha_id, max_age=max_age)

serializer.loads 方法接受一个 max_age 参数表示过期时间,如果时间超时,将返回空。

生成图片

生成图片用的是 captcha 库,首先是配置:

image = ImageCaptcha(width=160, height=60, fonts=[os.path.join(FONTS_PATH, 'CourierNew-Bold.ttf'),
                                                  os.path.join(FONTS_PATH, 'LiberationMono-Bold.ttf')])

可以指定图片大小,字体,字体大小。

返回图片有两种方式,一个是可以直接拿到图片的 IO 流(一个字节数组),或者直接持久化到硬盘上:

# 获得字节数组
data = image.generate(self.captcha_code)

# 将图片存储到 file_path 这个位置
file_path = tempfile.mkstemp(prefix="INFO-SUB-CAPTCHA-", suffix=".png")
image.write(self.captcha_code, file_path)

考虑到验证码的时效性,便没有采用持久化硬盘上这个操作,而是直接把图片流返回了。

封装的 Captcha 类:

class Captcha(object):
    def __init__(self, captcha_code=None):
        if captcha_code:
            self.captcha_code = captcha_code
        else:
            self.captcha_code = ''.join(random.sample(string.digits + string.lowercase + string.uppercase, 6))
        self.captcha_id = serializer.dumps(self.captcha_code)

    def image(self):
        data = image.generate(self.captcha_code)
        return data

    def save(self):
        file_path = tempfile.mkstemp(prefix="INFO-SUB-CAPTCHA-", suffix=".png")
        image.write(self.captcha_code, file_path)
        return file_path

    def validate(self, code):
        code = code.strip()
        if self.captcha_code.lower() == code.lower():
            return True
        return False

    @classmethod
    def get_by_captcha_id(cls, captcha_id, max_age=60 * 30):
        try:
            captcha_code = serializer.loads(captcha_id, max_age=max_age)
        except:
            captcha_code = None
        if not captcha_code:
            return None
        return cls(captcha_code)

获得验证码图片

因为使用的 flask,而 flask 有 send_file 函数将图片等文件返回:

@view_blueprint.route("/captcha/<captcha_id>")
def get_captcha(captcha_id):
    captcha = Captcha.get_by_captcha_id(captcha_id)
    if not captcha:
        abort(404)
    return send_file(captcha.image(), mimetype='image/png')

这样,直接访问带 captcha_id 的 url 将直接返回验证码图片。

<img src="{{ url_for('view.get_captcha', captcha_id=captcha_id) }}" width="100%" class="img-rounded">

验证操作

为了简单起见,我将验证操作写在了 WTF 表单中,比如登录表单:

class LoginForm(FlaskForm):
    username_or_email = StringField(u"用户名或注册邮箱", validators=[DataRequired(u"登录名不能为空")])
    password = PasswordField(u"密码", validators=[DataRequired(u"密码不能为空")])
    remember = BooleanField(u"记住我", default=False)
    captcha_code = StringField(u"验证码")
    captcha_id = HiddenField()

    def validate_password(self, field):
        user = get_user_by_username_or_email(self.username_or_email.data)
        if user and user.is_active and user.check_password(field.data):
            return
        raise ValidationError(u"用户名或密码错误")

    def validate_captcha_code(self, field):
        if current_app.config.get("DEBUG"):
            return
        if self.captcha_id.data:
            captcha = Captcha.get_by_captcha_id(self.captcha_id.data)
            if captcha.validate(field.data):
                return
        raise ValidationError(u"验证码错误")

在 view 层创建了 form 表单,在返回之前将 captcha_id 填充进去,在提交的时候,便会对验证码进行验证。