Création d'un Captcha avec Flask, WTForms, SQLAlchemy, SQLite
Respectez la vie privée de vos visiteurs, ne les connectez pas à un service tiers Captcha !

Dans le passé, j'ai écrit du code pour un Captcha (texte seul) pour un site web Flask . Il s'agit d'une mise à jour. Ici, j'utilise un paquet Pypi pour générer l'image. De plus, j'ai ajouté un bouton de rafraîchissement de l'image Captcha .
Vous pouvez essayer le code ci-dessous, il s'agit d'un formulaire d'inscription à une lettre d'information. Bien qu'il s'agisse d'une solution Captcha pour un site web Flask , elle peut être convertie en un serveur Captcha .
Notez que j'utilise SQLite ici uniquement à des fins de démonstration. N'utilisez pas SQLite dans un environnement multiuser !
Description de l'application
Notre application dispose d'un formulaire d'inscription (à une lettre d'information), dans lequel le visiteur peut saisir son adresse électronique. Ce formulaire est protégé par un Captcha.
Chaque fois que le formulaire est affiché, les données du Captcha (jeton, code, image) sont générées et stockées dans une base de données. Lorsque le formulaire est soumis, nous utilisons le jeton pour récupérer les données Captcha et comparer le code tapé par le visiteur au code stocké dans la base de données.
En outre, un bouton "rafraîchir" se trouve à côté de l'image. Lorsqu'il est cliqué, une nouvelle image est affichée.
Sur la page d'abonnement HTML , nous utilisons également :
- Bootstrap
- JQuery
Le bouton de rafraîchissement de la page Captcha utilise une icône Bootstrap .
The CaptchaField
Exigences pour notre nouveau champ CaptchaField :
- Un jeton unique, dans un champ caché
- Une balise img qui extrait l'image du serveur.
- Un champ de saisie de texte où le code peut être tapé.
- Une validation personnalisée pour vérifier le code saisi.
La principale difficulté ici est le jeton Captcha , que nous devons également stocker dans le champ.
Comme nous avons besoin d'une fonction de saisie de texte pour entrer le code Captcha , nous utilisons le code StringField de WTForms comme base pour notre 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 ""
Nous surchargeons les méthodes :
- __call__
- post_validate
Dans notre nouvelle méthode "__call__", nous effaçons le code précédemment saisi, nous générons un nouveau Captcha et nous ajoutons un HTML supplémentaire :
- Un champ caché pour stocker le jeton.
- Une balise img pour extraire l'image
- Un bouton pour rafraîchir l'image
Dans notre nouvelle méthode "post_validate", nous :
- Récupère le jeton et le code de la requête.
- Consulter l'enregistrement Captcha de la base de données à l'aide du jeton.
- Comparer le code avec le code stocké.
Nous créons une table de base de données 'Captcha' avec les champs suivants :
- jeton
- code
- données de l'image
J'ai créé une classe CaptchaUtils() pour créer un Captcha et valider un Captcha.
Une fois ces opérations effectuées, il ne reste plus qu'à ajouter un Captcha à un formulaire :
class SubscribeForm(wtf.FlaskForm):
...
captcha_code = CaptchaField(
'Enter code',
validators=[wtv.InputRequired()]
)
...
Pour rafraîchir la Captcha, nous appelons le serveur Flask pour générer une nouvelle Captcha. Cet appel renvoie le nouveau :
- captcha_token
- captcha_image_url
Sur la page d'abonnement, nous pouvons faire cela avec quelques lignes de 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);
});
});
});
Le code
Comme toujours, nous créons un virtual environment. Installer les paquets :
pip install flask
pip install flask-wtf
pip install sqlalchemy
pip install captcha
Ensuite, nous créons le répertoire 'project' , etc. Voici la structure du répertoire :
.
├── project
│ ├── app
│ │ ├── factory.py
│ │ └── templates
│ │ └── subscribe.html
│ └── run.py
Et voici les fichiers du projet, le fichier de la base de données SQLite apparaîtra dans le répertoire du projet :
- 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>
Résumé
Un Captcha est encore très utile aujourd'hui. Ici, nous avons utilisé un paquet Pypi pour générer l'image. Bien que le "flou" semble bon, il est plus vulnérable aux attaques de la machine à cause du code open source . Vous pouvez probablement faire mieux avec votre propre algorithme. Et en utilisant votre propre service Captcha au lieu d'un service Captcha tiers, vous protégez la vie privée de vos visiteurs !
Liens / crédits
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/
En savoir plus...
Captcha Flask SQLAlchemy WTForms
Récent
- Obtenir une liste des vidéos YouTube d'une personne
- De Docker-Composer à Docker Swarm : Configs
- Docker-Composer des projets avec des noms de services identiques
- X automatisation du web et scraping avec Selenium
- Aiohttp avec serveurs DNS personnalisés, Unbound et Docker
- Renvoyer uniquement les valeurs d'une liste d'enregistrements de FastAPI
Les plus consultés
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow