在 InfoSub 接入验证码的时候,第一次尝试使用验证码,设计的比较简单,利用的是 captcha
和 itsdangerous
模块。
在创建验证码的时候,后台随机生成一个字符串作为验证码(captcha_code),然后利用 itsdangerous.URLSafeTimedSerializer
对这个字符串进行编码,生成一个安全的(不能被随意反向编码,反向编码需要秘钥),方便的(可以直接扔到 url 里)的字符串。把这个字符串作为验证码的 id(captcha_id)。
这样使得校验验证码非常简单,利用秘钥对 captcha_id 反向编码便可以得到 captcha_code,将 captcha_code 与用户的输入进行比对便可以知道用户的验证码是否正确。
配置
首先要配置用于编码的 serializer
:
serializer = URLSafeTimedSerializer(SECRET_KEY)
这样便可以使用 serializer
的 loads
和 dumps
用于编码和反向编码:
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 填充进去,在提交的时候,便会对验证码进行验证。