angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Erstellen einer Captcha mit Flask, WTForms, SQLAlchemy, SQLite

Respektieren Sie die Privatsphäre Ihrer Besucher, verbinden Sie sie nicht mit einem Drittanbieterdienst Captcha !

10 April 2023
post main image

In der Vergangenheit habe ich etwas Code für einen (reinen Text) Captcha für eine Flask -Website geschrieben. Dies ist ein Update. Hier verwende ich ein Pypi-Paket, um das Bild zu generieren. Außerdem habe ich einen Captcha -Bildaktualisierungsbutton hinzugefügt.
Sie können den Code unten ausprobieren, es ist ein (Newsletter-)Anmeldeformular. Obwohl dies eine Captcha -Lösung für eine Flask -Website ist, kann sie in einen Captcha -Server umgewandelt werden.

Beachten Sie, dass ich SQLite hier nur zu Demonstrationszwecken verwende. Verwenden Sie SQLite nicht in einer multiuser Umgebung!

Beschreibung

Unsere Anwendung hat ein (Newsletter-)Anmeldeformular, in das der Besucher seine E-Mail-Adresse eintragen kann. Dieses Formular ist durch eine Captcha geschützt.

Jedes Mal, wenn das Formular angezeigt wird, werden die Captcha -Daten (Token, Code, Bild) erzeugt und in einer Datenbank gespeichert. Wenn das Formular abgeschickt wird, verwenden wir das Token, um die Captcha -Daten abzurufen, und vergleichen den vom Besucher eingegebenen Code mit dem in der Datenbank gespeicherten Code.

Außerdem befindet sich neben dem Bild eine Schaltfläche "Aktualisieren". Wird diese angeklickt, wird ein neues Bild angezeigt.

Auf der Seite des Abonnements HTML verwenden wir auch:

  • Bootstrap
  • JQuery

Die Aktualisierungsschaltfläche Captcha verwendet ein Bootstrap -Symbol.

The CaptchaField

Anforderungen für unser neues CaptchaField:

  • Ein eindeutiges Token, in einem versteckten Feld
  • Ein img-Tag, das das Bild vom Server abruft.
  • Ein Texteingabefeld, in das der Code eingegeben werden kann.
  • Benutzerdefinierte Validierung zur Überprüfung des eingegebenen Codes.

Die Hauptschwierigkeit ist hier das Captcha -Token, das wir auch in dem Feld speichern müssen.

Da wir eine Texteingabefunktion benötigen, um den Captcha -Code einzugeben, verwenden wir den StringField-Code aus WTForms als Grundlage für unser 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 ""

Wir überschreiben die Methoden:

  • __call__
  • post_validate

In unserer neuen '__call__'-Methode löschen wir den zuvor eingegebenen Code, erzeugen einen neuen Captcha und fügen dann einen zusätzlichen HTML hinzu:

  • Ein verstecktes Feld zum Speichern des Tokens.
  • Ein img-Tag zum Abrufen des Bildes
  • Eine Schaltfläche zum Aktualisieren des Bildes

In unserer neuen "post_validate"-Methode werden wir:

  • Abrufen des Tokens und des Codes aus der Anfrage.
  • Abfrage des Datenbankeintrags Captcha unter Verwendung des Tokens.
  • Vergleich des Codes mit dem gespeicherten Code.

Wir erstellen eine Datenbanktabelle 'Captcha' mit den folgenden Feldern:

  • Token
  • Code
  • Bilddaten

Ich habe eine Klasse CaptchaUtils() erstellt, um eine Captcha zu erstellen und eine Captcha zu validieren.

Sobald dies erledigt ist, müssen wir nur noch eine Captcha zu einem Formular hinzufügen:

class SubscribeForm(wtf.FlaskForm):
    ...
    captcha_code = CaptchaField(
		'Enter code',
        validators=[wtv.InputRequired()]
    )
    ...

Um die Captcha zu aktualisieren, rufen wir den Flask -Server auf, um eine neue Captcha zu erzeugen. Dieser Aufruf liefert die neue:

  • captcha_token
  • captcha_image_url

Auf der Anmeldeseite können wir dies mit ein paar Zeilen Javascript (JQuery) erreichen:

$(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);
		});
	});
});

Der Code

Wie immer erstellen wir eine virtual environment. Installieren Sie die Pakete:

pip install flask
pip install flask-wtf
pip install sqlalchemy
pip install captcha

Als nächstes erstellen wir das Verzeichnis 'project' , usw. Hier ist die Verzeichnisstruktur:

.
├── project
│   ├── app
│   │   ├── factory.py
│   │   └── templates
│   │       └── subscribe.html
│   └── run.py

Und hier sind die Dateien im Projekt, die SQLite Datenbankdatei wird im Projektverzeichnis erscheinen:

  • run.py
  • factory.py
  • abonnieren.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>

Zusammenfassung

Ein Captcha ist auch heute noch sehr nützlich. Hier haben wir ein Pypi-Paket verwendet, um das Bild zu erzeugen. Obwohl die "Unschärfe" gut aussieht, ist sie wegen des open source -Codes anfälliger für maschinelle Angriffe. Mit Ihrem eigenen Algorithmus können Sie es wahrscheinlich besser machen. Und indem Sie Ihren eigenen Captcha anstelle eines Captcha -Dienstes eines Dritten verwenden, schützen Sie die Privatsphäre Ihrer Besucher!

Links / Impressum

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/