Создание Captcha с Flask, WTForms, SQLAlchemy, SQLite
Уважайте конфиденциальность ваших посетителей, не подключайте их к стороннему сервису Captcha !
В прошлом я написал некоторый код (только для текста) Captcha для сайта Flask . Это обновление. Здесь я использую пакет Pypi для генерации изображения. Кроме того, я добавил кнопку обновления изображения Captcha .
Вы можете попробовать код ниже, это форма подписки (на рассылку). Хотя это решение Captcha для сайта Flask , его можно преобразовать в сервер Captcha .
Обратите внимание, что я использую SQLite здесь только в демонстрационных целях. Не используйте SQLite в среде с несколькими user !
Описание
В нашем приложении есть форма подписки (на рассылку новостей), в которую посетитель может ввести свой адрес электронной почты. Эта форма защищена Captcha.
Каждый раз, когда форма отображается, данные Captcha (токен, код, изображение) генерируются и сохраняются в базе данных. Когда форма отправляется, мы используем токен для получения данных Captcha и сравниваем код, введенный посетителем, с кодом, хранящимся в базе данных.
Кроме того, рядом с изображением находится кнопка "обновить". При нажатии на нее отображается новое изображение.
На странице подписки HTML мы также используем:
- Bootstrap
- JQuery
Кнопка обновления Captcha использует иконку Bootstrap .
The CaptchaField
Требования к нашему новому полю CaptchaField:
- Уникальный токен, в скрытом поле
- img-тег, который берет изображение с сервера.
- Текстовое поле ввода, в которое можно ввести код.
- Пользовательская валидация для проверки введенного кода.
Основная сложность здесь заключается в токене Captcha , мы должны хранить его также в поле.
Поскольку нам нужна функция ввода текста для ввода кода Captcha , мы используем код StringField из WTForms как основу для нашего CaptchaField.
class StringField(Field):
"""
This field is the base for most of the more complicated fields, and
represents an ``<input type="text">``.
"""
widget = widgets.TextInput()
def process_formdata(self, valuelist):
if valuelist:
self.data = valuelist[0]
def _value(self):
return str(self.data) if self.data is not None else ""
Мы переопределяем методы:
- __call__
- post_validate
В нашем новом методе '__call__' мы очищаем ранее введенный код, генерируем новый Captcha и затем добавляем дополнительный HTML:
- Скрытое поле для хранения маркера.
- img-тег для извлечения изображения
- Кнопка для обновления изображения
В нашем новом методе 'post_validate' мы:
- Получаем токен и код из запроса.
- Просматриваем запись в базе данных Captcha с помощью маркера.
- Сравниваем код с сохраненным кодом.
Мы создаем таблицу базы данных 'Captcha' со следующими полями:
- токен
- код
- данные изображения
Я создал класс CaptchaUtils() для создания Captcha и проверки Captcha.
Как только все вышеперечисленное будет сделано, нам останется только добавить Captcha в форму:
class SubscribeForm(wtf.FlaskForm):
...
captcha_code = CaptchaField(
'Enter code',
validators=[wtv.InputRequired()]
)
...
Чтобы обновить Captcha, мы вызываем сервер Flask для генерации нового Captcha. Этот вызов возвращает новое значение:
- captcha_token
- captcha_image_url
На странице подписки мы можем сделать это с помощью нескольких строк Javascript (JQuery):
$(document).ready(function(){
$('#captcha-refresh-button').click(function(){
captcha_new_url = '/captcha/new';
$.getJSON(captcha_new_url, function(data){
$('#captcha-token').val(data.captcha_token);
$('#captcha-image').attr('src', data.captcha_image_url);
});
});
});
Код
Как обычно, мы создаем virtual environment. Устанавливаем пакеты:
pip install flask
pip install flask-wtf
pip install sqlalchemy
pip install captcha
Далее создаем директорию 'project' и т.д. Вот структура каталогов:
.
├── project
│ ├── app
│ │ ├── factory.py
│ │ └── templates
│ │ └── subscribe.html
│ └── run.py
А вот файлы в проекте, файл базы данных SQLite появится в директории проекта:
- run.py
- factory.py
- subscribe.html
# run.py
from app.factory import create_app
app = create_app('development')
if __name__ == '__main__':
app.run(
host= '127.0.0.1',
port=5555,
debug=True,
use_reloader=True,
)
# factory.py
import datetime
import io
import logging
import os
import random
import sys
import uuid
from flask import current_app, Flask, jsonify, make_response, render_template, request, url_for
import markupsafe
import sqlalchemy as sa
import sqlalchemy.orm as orm
import flask_wtf as wtf
import wtforms as wt
import wtforms.validators as wtv
import wtforms.widgets as wtw
from captcha.image import ImageCaptcha
class Base(orm.DeclarativeBase):
pass
class Captcha(Base):
__tablename__ = 'captcha'
id = sa.Column(sa.Integer, primary_key=True)
created_on = sa.Column(sa.DateTime, server_default=sa.func.now())
token = sa.Column(sa.String(40), index=True)
code = sa.Column(sa.String(10))
image = sa.Column(sa.LargeBinary)
def __str__(self):
return '<Captcha: id = {}, token = {}, code = {}>'.format(id, token, code)
# create table
engine = sa.create_engine('sqlite:///flask_captcha.sqlite', echo=True)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# get scoped session for web
db_session = orm.scoped_session(orm.sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
)
class CaptchaUtils:
code_chars = ['A', 'b', 'C', 'd', 'e', 'f', 'h', 'K', 'M', 'N', 'p', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z']
code_nums = ['2', '3', '4', '6', '7', '8', '9']
@classmethod
def get_random_code(cls, n):
return ''.join(random.choice(cls.code_chars + cls.code_nums) for i in range(n))
@classmethod
def create(cls, db):
# 1. create captcha token
captcha_token = uuid.uuid4().hex
# 2. create captcha code
captcha_code = cls.get_random_code(5)
# 3. create captcha image
im = ImageCaptcha()
im_buf = io.BytesIO()
im.write(captcha_code, im_buf, format='png')
captcha_image = im_buf.getvalue()
# 4. store in db
captcha = Captcha(
token=captcha_token,
code=captcha_code,
image=captcha_image
)
db.add(captcha)
db.commit()
return captcha_token, url_for('captcha_image', token=captcha_token)
@classmethod
def validate(cls, db, token, code):
stmt = sa.select(Captcha).where(Captcha.token == token)
captcha = db_session.execute(stmt).scalars().first()
if captcha is None:
return False
if code.lower() != captcha.code.lower():
return False
return True
class CaptchaField(wt.Field):
widget = wtw.TextInput()
def process_formdata(self, valuelist):
if valuelist:
self.data = valuelist[0]
def _value(self):
return str(self.data) if self.data is not None else ""
def __call__(self, *args, **kwargs):
self.data = ''
input_field_html = super(CaptchaField, self).__call__(*args,**kwargs)
captcha_token, captcha_image_url = CaptchaUtils.create(db_session)
hidden_field_html = markupsafe.Markup('<input type="hidden" name="captcha_token" value="{}" id="captcha-token">'.format(captcha_token))
image_html = markupsafe.Markup('<img src="{}" class="border mb-2" id="captcha-image">'.format(captcha_image_url))
button_html = markupsafe.Markup("""<button type="button" class="btn btn-outline-secondary btn-sm ms-1" id="captcha-refresh-button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg></button>""")
break_html = markupsafe.Markup('<br>')
return hidden_field_html + image_html + button_html + break_html + input_field_html
def post_validate(self, form, validation_stopped):
if not CaptchaUtils.validate(
db_session,
request.form.get('captcha_token', None),
form.captcha_code.data
):
raise wtv.ValidationError('Captcha code invalid')
class SubscribeForm(wtf.FlaskForm):
email = wt.StringField(
'Email',
validators=[wtv.InputRequired(), wtv.Length(min=4, max=100)]
)
captcha_code = CaptchaField(
'Enter code',
validators=[wtv.InputRequired()]
)
def create_app(deploy_config):
app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.config.update(
SECRET_KEY='your-secret-key',
DEPLOY_CONFIG=deploy_config,
)
app.logger.setLevel(logging.DEBUG)
# routes
@app.route('/')
def index():
return 'Home'
@app.route('/subscribe', methods=('GET', 'POST'))
def subscribe():
form = SubscribeForm()
if form.validate_on_submit():
return 'Thank you'
return render_template(
'subscribe.html',
form=form,
)
@app.route('/captcha/image/<token>', methods=('GET', 'POST'))
def captcha_image(token):
stmt = sa.select(Captcha).where(Captcha.token == token)
captcha = db_session.execute(stmt).scalars().first()
if captcha is None:
return ''
response = make_response(captcha.image)
response.headers.set('Content-Type', 'image/png')
return response
@app.route('/captcha/new', methods=('GET', 'POST'))
def captcha_new():
captcha_token, captcha_image_url = CaptchaUtils.create(db_session)
return jsonify({
'captcha_token': captcha_token,
'captcha_image_url': captcha_image_url
})
return app
<!DOCTYPE html>
<html lang = "en">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h3 class="my-3">
Subscribe to our newsletter
</h3>
<form method="post">
{{ form.csrf_token }}
<div class="row mt-2">
<div class="col-2">
{{ form.email.label }}
</div>
<div class="col-10">
{{ form.email() }}
{% if form.email.errors %}
<ul class="text-danger">
{% for error in form.email.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
<div class="row mt-2">
<div class="col-2">
{{ form.captcha_code.label }}
</div>
<div class="col-10">
{{ form.captcha_code() }}
{% if form.captcha_code.errors %}
<ul class="text-danger">
{% for error in form.captcha_code.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
<div class="row mt-2">
<div class="col-2"></div>
<div class="col-10">
<input type="submit" value="Submit" class="btn btn-primary">
</div>
</div>
</form>
</div>
<script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
<script>
$(document).ready(function(){
$('#captcha-refresh-button').click(function(){
captcha_new_url = '/captcha/new';
$.getJSON(captcha_new_url, function(data){
$('#captcha-token').val(data.captcha_token);
$('#captcha-image').attr('src', data.captcha_image_url);
});
});
});
</script>
</body>
</html>
Резюме
Captcha все еще очень полезен сегодня. Здесь мы использовали пакет Pypi для генерации изображения. Хотя "размытие" выглядит хорошо, оно более уязвимо для машинных атак из-за кода open source . Вероятно, вы можете добиться большего с помощью собственного алгоритма. А используя собственный обслуживаемый Captcha вместо стороннего сервиса Captcha , вы защищаете конфиденциальность своих посетителей!
Ссылки / кредиты
Another captcha implementation for Flask and WTForms
https://www.peterspython.com/en/blog/another-captcha-implementation-for-flask-and-wtforms
Captcha
https://pypi.org/project/captcha
DB Browser for SQLite
https://sqlitebrowser.org
WTForms
https://wtforms.readthedocs.io/en/3.0.x/
Подробнее
Captcha Flask SQLAlchemy WTForms
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу