Een Captcha maken met Flask, WTForms, SQLAlchemy, SQLite
Respecteer de privacy van uw bezoekers, verbind ze niet met een dienst van derden Captcha !
In het verleden heb ik wat code geschreven voor een (text-only) Captcha voor een Flask website. Dit is een update. Hier gebruik ik een Pypi pakket om de afbeelding te genereren. Daarnaast heb ik ook een Captcha image refresh button toegevoegd.
Je kunt onderstaande code proberen, het is een (nieuwsbrief) inschrijfformulier. Hoewel dit een Captcha oplossing is voor een Flask website, kan het worden omgezet in een Captcha server.
Merk op dat ik SQLite hier alleen voor demonstratiedoeleinden gebruik. Gebruik SQLite niet in een multi-user omgeving!
Beschrijving
Onze applicatie heeft een (nieuwsbrief) inschrijf-formulier, waar de bezoeker zijn email adres kan intypen. Dit formulier wordt beschermd door een Captcha.
Telkens wanneer het formulier wordt getoond, wordt de Captcha data (token, code, afbeelding) gegenereerd en opgeslagen in een database. Wanneer het formulier wordt verzonden, gebruiken wij het token om de Captcha gegevens op te halen, en vergelijken wij de door de bezoeker ingetypte code met de in de database opgeslagen code.
Daarnaast is er een 'ververs'-knop naast de afbeelding. Wanneer daarop wordt geklikt, wordt een nieuwe afbeelding getoond.
Op de inschrijvingspagina HTML gebruiken we ook:
- Bootstrap
- JQuery
De Captcha verversingsknop gebruikt een Bootstrap pictogram.
The CaptchaField
Vereisten voor ons nieuwe CaptchaField:
- Een uniek token, in een verborgen veld
- Een img-tag die de afbeelding van de server haalt.
- Een tekstinvoerveld waar de code kan worden ingetypt.
- Aangepaste validatie om de getypte code te controleren.
De grootste moeilijkheid hier is het token Captcha , dat we ook in het veld moeten opslaan.
Omdat we een tekstinvoerfunctie nodig hebben om de Captcha code in te voeren, gebruiken we de StringField code uit WTForms als basis voor ons 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 ""
We overschrijven de methoden:
- __call__
- post_valideren
In onze nieuwe '__call__'-methode wissen we de eerder ingevoerde code, genereren we een nieuwe een Captcha en voegen dan extra HTML toe:
- Een verborgen veld om het token op te slaan.
- Een img-tag om de afbeelding op te halen
- Een knop om de afbeelding te verversen
In onze nieuwe 'post_validate' methode:
- Het token en de code ophalen uit het verzoek.
- De database Captcha record opzoeken met behulp van het token.
- Vergelijken de code met de opgeslagen code.
We maken een databasetabel "Captcha" aan met de volgende velden:
- token
- code
- beeldgegevens
Ik heb een klasse CaptchaUtils() gemaakt om een Captcha aan te maken en een Captcha te valideren.
Zodra het bovenstaande is gedaan, hoeven we alleen nog maar te doen wanneer we een Captcha aan een formulier willen toevoegen:
class SubscribeForm(wtf.FlaskForm):
...
captcha_code = CaptchaField(
'Enter code',
validators=[wtv.InputRequired()]
)
...
Om de Captcha te vernieuwen, roepen we de Flask -server aan om een nieuwe Captcha te genereren. Deze oproep geeft de nieuwe:
- captcha_token
- captcha_image_url
Op de inschrijfpagina kunnen we dit doen met een paar regels 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);
});
});
});
De code
Zoals altijd maken we een virtual environment aan. Installeer de pakketten:
pip install flask
pip install flask-wtf
pip install sqlalchemy
pip install captcha
Vervolgens maken we de map 'project' aan, enz. Hier is de directory-structuur:
.
├── project
│ ├── app
│ │ ├── factory.py
│ │ └── templates
│ │ └── subscribe.html
│ └── run.py
En hier zijn de bestanden in het project, het databasebestand SQLite komt in de projectdirectory:
- 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>
Samenvatting
Een Captcha is vandaag de dag nog steeds erg nuttig. Hier hebben we een Pypi-pakket gebruikt om de afbeelding te genereren. Hoewel de 'blurring' er goed uitziet, is het kwetsbaarder voor machine-aanvallen vanwege de open source code. U kunt het waarschijnlijk beter doen met uw eigen algoritme. En door uw eigen geserveerde Captcha te gebruiken in plaats van een Captcha dienst van derden, beschermt u de privacy van uw bezoekers!
Links / credits
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/
Lees meer
Captcha Flask SQLAlchemy WTForms
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's