Formulare bilden das Herzstück der Benutzerinteraktion in Webanwendungen. Sie ermöglichen die Übertragung von Daten vom Browser zum Server und stellen somit die primäre Schnittstelle für Eingaben dar. Um Formulare effektiv zu verstehen, müssen wir zunächst die grundlegenden HTML-Konzepte betrachten und dann deren Integration in Flask erlernen.
Ein HTML-Formular besteht aus einem Container-Element und
verschiedenen Eingabefeldern. Das form-Element definiert dabei die
Grenzen des Formulars und bestimmt, wie die Daten übertragen werden.
Zwei Attribute sind dabei von zentraler Bedeutung: method
bestimmt die HTTP-Methode für die Datenübertragung, während
action die Ziel-URL definiert.
<!-- Grundstruktur eines HTML-Formulars -->
<form method="POST" action="{{ url_for('process_form') }}">
<!-- Eingabefelder kommen hier hinein -->
<input type="text" name="benutzername" required>
<input type="email" name="email" required>
<button type="submit">Absenden</button>
</form>Die method-Eigenschaft kann zwei Werte annehmen: GET
überträgt Daten in der URL und eignet sich für Suchformulare, während
POST Daten im Request-Body sendet und für alle anderen Formulare
verwendet werden sollte. POST bietet bessere Sicherheit und kann größere
Datenmengen verarbeiten.
HTML5 bietet eine Vielzahl von Eingabefeld-Typen, die jeweils spezifische Datentypen verarbeiten und unterschiedliche Validierungsmechanismen bieten. Das Verständnis dieser Typen ist fundamental für die Erstellung benutzerfreundlicher Formulare.
<!-- templates/forms/benutzer_registrierung.html -->
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<form method="POST" class="needs-validation" novalidate>
<!-- Text-Eingaben -->
<div class="mb-3">
<label for="vorname" class="form-label">Vorname</label>
<input type="text"
class="form-control"
id="vorname"
name="vorname"
required
minlength="2"
maxlength="50">
<div class="invalid-feedback">
Bitte geben Sie einen gültigen Vornamen ein (2-50 Zeichen).
</div>
</div>
<!-- E-Mail-Eingabe mit eingebauter Validierung -->
<div class="mb-3">
<label for="email" class="form-label">E-Mail-Adresse</label>
<input type="email"
class="form-control"
id="email"
name="email"
required>
<div class="invalid-feedback">
Bitte geben Sie eine gültige E-Mail-Adresse ein.
</div>
</div>
<!-- Passwort-Eingabe -->
<div class="mb-3">
<label for="passwort" class="form-label">Passwort</label>
<input type="password"
class="form-control"
id="passwort"
name="passwort"
required
minlength="8">
<div class="form-text">
Das Passwort muss mindestens 8 Zeichen lang sein.
</div>
<div class="invalid-feedback">
Das Passwort ist zu kurz.
</div>
</div>
<!-- Telefonnummer mit Pattern-Validierung -->
<div class="mb-3">
<label for="telefon" class="form-label">Telefonnummer</label>
<input type="tel"
class="form-control"
id="telefon"
name="telefon"
pattern="[0-9\s\-\+\(\)]+"
placeholder="+49 123 456789">
<div class="invalid-feedback">
Bitte geben Sie eine gültige Telefonnummer ein.
</div>
</div>
<!-- Datum-Eingabe -->
<div class="mb-3">
<label for="geburtsdatum" class="form-label">Geburtsdatum</label>
<input type="date"
class="form-control"
id="geburtsdatum"
name="geburtsdatum"
max="{{ today }}">
<div class="invalid-feedback">
Bitte geben Sie ein gültiges Geburtsdatum ein.
</div>
</div>
<!-- Auswahl-Felder -->
<div class="mb-3">
<label for="land" class="form-label">Land</label>
<select class="form-select" id="land" name="land" required>
<option value="">Bitte wählen...</option>
<option value="DE">Deutschland</option>
<option value="AT">Österreich</option>
<option value="CH">Schweiz</option>
</select>
<div class="invalid-feedback">
Bitte wählen Sie ein Land aus.
</div>
</div>
<!-- Checkbox-Gruppe -->
<div class="mb-3">
<label class="form-label">Interessen</label>
<div class="form-check">
<input class="form-check-input" type="checkbox"
value="technik" id="interesse_technik" name="interessen">
<label class="form-check-label" for="interesse_technik">
Technik
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
value="sport" id="interesse_sport" name="interessen">
<label class="form-check-label" for="interesse_sport">
Sport
</label>
</div>
</div>
<!-- Radio-Buttons -->
<div class="mb-3">
<label class="form-label">Anrede</label>
<div class="form-check">
<input class="form-check-input" type="radio"
name="anrede" id="anrede_herr" value="herr" required>
<label class="form-check-label" for="anrede_herr">
Herr
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
name="anrede" id="anrede_frau" value="frau" required>
<label class="form-check-label" for="anrede_frau">
Frau
</label>
</div>
</div>
<!-- Textarea für längere Texte -->
<div class="mb-3">
<label for="kommentar" class="form-label">Kommentar</label>
<textarea class="form-control"
id="kommentar"
name="kommentar"
rows="4"
maxlength="500"></textarea>
<div class="form-text">
Maximal 500 Zeichen.
</div>
</div>
<!-- Datenschutz-Checkbox (erforderlich) -->
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox"
id="datenschutz" name="datenschutz" required>
<label class="form-check-label" for="datenschutz">
Ich stimme der <a href="{{ url_for('datenschutz') }}">Datenschutzerklärung</a> zu
</label>
<div class="invalid-feedback">
Sie müssen der Datenschutzerklärung zustimmen.
</div>
</div>
<button type="submit" class="btn btn-primary">Registrieren</button>
</form>
</div>
</div>
</div>
{% endblock %}Die serverseitige Verarbeitung von Formulardaten erfordert das Verständnis des Flask Request-Objekts und der verschiedenen Datenstrukturen, die Formulare erzeugen können.
from flask import Flask, request, render_template, redirect, url_for, flash
from datetime import datetime, date
app = Flask(__name__)
app.secret_key = 'ihr-geheimer-schluessel'
@app.route('/registrierung', methods=['GET', 'POST'])
def benutzer_registrierung():
if request.method == 'GET':
# Formular anzeigen
return render_template('forms/benutzer_registrierung.html',
today=date.today().isoformat())
elif request.method == 'POST':
# Formulardaten verarbeiten
formulardaten = {
'vorname': request.form.get('vorname', '').strip(),
'email': request.form.get('email', '').strip(),
'passwort': request.form.get('passwort', ''),
'telefon': request.form.get('telefon', '').strip(),
'geburtsdatum': request.form.get('geburtsdatum'),
'land': request.form.get('land'),
'interessen': request.form.getlist('interessen'), # Mehrere Werte
'anrede': request.form.get('anrede'),
'kommentar': request.form.get('kommentar', '').strip(),
'datenschutz': request.form.get('datenschutz') == 'on'
}
# Grundlegende Validierung
fehler = validiere_registrierungsdaten(formulardaten)
if fehler:
# Fehler gefunden - Formular erneut anzeigen
for fehler_text in fehler:
flash(fehler_text, 'error')
return render_template('forms/benutzer_registrierung.html',
today=date.today().isoformat(),
**formulardaten)
# Daten verarbeiten (z.B. in Datenbank speichern)
erfolg = speichere_benutzer(formulardaten)
if erfolg:
flash('Registrierung erfolgreich!', 'success')
return redirect(url_for('login'))
else:
flash('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', 'error')
return render_template('forms/benutzer_registrierung.html',
today=date.today().isoformat(),
**formulardaten)
def validiere_registrierungsdaten(daten):
"""
Validiert die Registrierungsdaten und gibt eine Liste von Fehlern zurück.
Diese Funktion zeigt das Grundprinzip der serverseitigen Validierung.
"""
fehler = []
# Vorname prüfen
if not daten['vorname']:
fehler.append('Vorname ist erforderlich.')
elif len(daten['vorname']) < 2:
fehler.append('Vorname muss mindestens 2 Zeichen lang sein.')
elif len(daten['vorname']) > 50:
fehler.append('Vorname darf maximal 50 Zeichen lang sein.')
# E-Mail prüfen
if not daten['email']:
fehler.append('E-Mail-Adresse ist erforderlich.')
elif '@' not in daten['email'] or '.' not in daten['email']:
fehler.append('Bitte geben Sie eine gültige E-Mail-Adresse ein.')
# Passwort prüfen
if not daten['passwort']:
fehler.append('Passwort ist erforderlich.')
elif len(daten['passwort']) < 8:
fehler.append('Passwort muss mindestens 8 Zeichen lang sein.')
# Land prüfen
erlaubte_laender = ['DE', 'AT', 'CH']
if daten['land'] and daten['land'] not in erlaubte_laender:
fehler.append('Bitte wählen Sie ein gültiges Land aus.')
# Anrede prüfen
if daten['anrede'] not in ['herr', 'frau']:
fehler.append('Bitte wählen Sie eine Anrede aus.')
# Datenschutz prüfen
if not daten['datenschutz']:
fehler.append('Sie müssen der Datenschutzerklärung zustimmen.')
# Geburtsdatum prüfen (falls angegeben)
if daten['geburtsdatum']:
try:
geburtsdatum = datetime.strptime(daten['geburtsdatum'], '%Y-%m-%d').date()
if geburtsdatum > date.today():
fehler.append('Geburtsdatum darf nicht in der Zukunft liegen.')
except ValueError:
fehler.append('Ungültiges Geburtsdatum.')
return fehler
def speichere_benutzer(daten):
"""
Speichert die Benutzerdaten in der Datenbank.
In einer echten Anwendung würde hier die Datenbanklogik stehen.
"""
try:
# Hier würde die Datenbankoperation stattfinden
# z.B. mit SQLAlchemy oder PyODBC für MS-SQL
print(f"Speichere Benutzer: {daten['email']}")
return True
except Exception as e:
print(f"Fehler beim Speichern: {e}")
return FalseDatenvalidierung ist ein kritischer Aspekt der Anwendungssicherheit und Datenintegrität. Sie findet auf mehreren Ebenen statt: im Browser durch HTML5-Validierung, auf dem Server durch Python-Code und in der Datenbank durch Constraints. Eine robuste Anwendung implementiert alle drei Ebenen, da clientseitige Validierung allein niemals ausreicht.
Serverseitige Validierung ist unverzichtbar, weil Benutzer die clientseitige Validierung umgehen können. Jede Eingabe, die vom Client kommt, muss als potentiell gefährlich betrachtet und validiert werden. Flask bietet verschiedene Ansätze für die Validierung, von manueller Prüfung bis hin zu strukturierten Validierungsbibliotheken.
import re
from datetime import datetime, date
from typing import Dict, List, Any
class FormValidator:
"""
Eine Klasse für strukturierte Formularvalidierung.
Sie zeigt das Prinzip auf, wie Validierungslogik organisiert werden kann.
"""
def __init__(self):
# E-Mail-Pattern für grundlegende Validierung
self.email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
# Telefon-Pattern (vereinfacht)
self.phone_pattern = re.compile(r'^[\d\s\-\+\(\)]+$')
def validate_required(self, value: str, field_name: str) -> List[str]:
"""Prüft, ob ein Pflichtfeld ausgefüllt ist."""
errors = []
if not value or not value.strip():
errors.append(f'{field_name} ist erforderlich.')
return errors
def validate_string_length(self, value: str, field_name: str,
min_length: int = 0, max_length: int = None) -> List[str]:
"""Validiert die Länge eines String-Feldes."""
errors = []
if value: # Nur validieren wenn Wert vorhanden
length = len(value.strip())
if length < min_length:
errors.append(f'{field_name} muss mindestens {min_length} Zeichen lang sein.')
if max_length and length > max_length:
errors.append(f'{field_name} darf maximal {max_length} Zeichen lang sein.')
return errors
def validate_email(self, email: str) -> List[str]:
"""Validiert eine E-Mail-Adresse."""
errors = []
if email and not self.email_pattern.match(email):
errors.append('Bitte geben Sie eine gültige E-Mail-Adresse ein.')
return errors
def validate_phone(self, phone: str) -> List[str]:
"""Validiert eine Telefonnummer."""
errors = []
if phone and not self.phone_pattern.match(phone):
errors.append('Bitte geben Sie eine gültige Telefonnummer ein.')
return errors
def validate_date(self, date_string: str, field_name: str,
max_date: date = None, min_date: date = None) -> List[str]:
"""Validiert ein Datum."""
errors = []
if date_string:
try:
parsed_date = datetime.strptime(date_string, '%Y-%m-%d').date()
if max_date and parsed_date > max_date:
errors.append(f'{field_name} darf nicht nach dem {max_date.strftime("%d.%m.%Y")} liegen.')
if min_date and parsed_date < min_date:
errors.append(f'{field_name} darf nicht vor dem {min_date.strftime("%d.%m.%Y")} liegen.')
except ValueError:
errors.append(f'Ungültiges Datum für {field_name}.')
return errors
def validate_choice(self, value: str, choices: List[str], field_name: str) -> List[str]:
"""Validiert, ob ein Wert in einer Liste erlaubter Werte enthalten ist."""
errors = []
if value and value not in choices:
errors.append(f'Ungültiger Wert für {field_name}.')
return errors
def validate_password_strength(self, password: str) -> List[str]:
"""Validiert die Passwortstärke."""
errors = []
if not password:
return errors # Wird durch required-Validierung abgefangen
if len(password) < 8:
errors.append('Passwort muss mindestens 8 Zeichen lang sein.')
if not re.search(r'[A-Z]', password):
errors.append('Passwort muss mindestens einen Großbuchstaben enthalten.')
if not re.search(r'[a-z]', password):
errors.append('Passwort muss mindestens einen Kleinbuchstaben enthalten.')
if not re.search(r'\d', password):
errors.append('Passwort muss mindestens eine Zahl enthalten.')
return errors
# Verwendung des Validators in einer Flask-Route
@app.route('/erweiterte-registrierung', methods=['GET', 'POST'])
def erweiterte_registrierung():
if request.method == 'GET':
return render_template('forms/erweiterte_registrierung.html')
# Formulardaten extrahieren
form_data = {
'vorname': request.form.get('vorname', '').strip(),
'nachname': request.form.get('nachname', '').strip(),
'email': request.form.get('email', '').strip(),
'passwort': request.form.get('passwort', ''),
'passwort_wiederholen': request.form.get('passwort_wiederholen', ''),
'telefon': request.form.get('telefon', '').strip(),
'geburtsdatum': request.form.get('geburtsdatum'),
'land': request.form.get('land'),
'agb_akzeptiert': request.form.get('agb_akzeptiert') == 'on'
}
# Validierung durchführen
validator = FormValidator()
all_errors = []
# Vorname validieren
all_errors.extend(validator.validate_required(form_data['vorname'], 'Vorname'))
all_errors.extend(validator.validate_string_length(form_data['vorname'], 'Vorname', 2, 50))
# Nachname validieren
all_errors.extend(validator.validate_required(form_data['nachname'], 'Nachname'))
all_errors.extend(validator.validate_string_length(form_data['nachname'], 'Nachname', 2, 50))
# E-Mail validieren
all_errors.extend(validator.validate_required(form_data['email'], 'E-Mail'))
all_errors.extend(validator.validate_email(form_data['email']))
# Passwort validieren
all_errors.extend(validator.validate_required(form_data['passwort'], 'Passwort'))
all_errors.extend(validator.validate_password_strength(form_data['passwort']))
# Passwort-Wiederholung prüfen
if form_data['passwort'] != form_data['passwort_wiederholen']:
all_errors.append('Passwörter stimmen nicht überein.')
# Telefon validieren (optional)
if form_data['telefon']:
all_errors.extend(validator.validate_phone(form_data['telefon']))
# Geburtsdatum validieren (optional)
if form_data['geburtsdatum']:
all_errors.extend(validator.validate_date(form_data['geburtsdatum'], 'Geburtsdatum',
max_date=date.today()))
# Land validieren
erlaubte_laender = ['DE', 'AT', 'CH', 'FR', 'IT']
all_errors.extend(validator.validate_choice(form_data['land'], erlaubte_laender, 'Land'))
# AGB-Akzeptierung prüfen
if not form_data['agb_akzeptiert']:
all_errors.append('Sie müssen den Allgemeinen Geschäftsbedingungen zustimmen.')
# Weitere geschäftsspezifische Validierungen
# Beispiel: E-Mail-Eindeutigkeit prüfen
if form_data['email'] and not all_errors:
if email_bereits_registriert(form_data['email']):
all_errors.append('Diese E-Mail-Adresse ist bereits registriert.')
if all_errors:
# Fehler gefunden - Formular mit Fehlern anzeigen
for error in all_errors:
flash(error, 'error')
return render_template('forms/erweiterte_registrierung.html', **form_data)
# Validierung erfolgreich - Benutzer speichern
if registriere_benutzer(form_data):
flash('Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success')
return redirect(url_for('login'))
else:
flash('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', 'error')
return render_template('forms/erweiterte_registrierung.html', **form_data)
def email_bereits_registriert(email: str) -> bool:
"""
Prüft, ob eine E-Mail-Adresse bereits in der Datenbank vorhanden ist.
In einer echten Anwendung würde hier eine Datenbankabfrage stattfinden.
"""
# Simulation einer Datenbankabfrage
return email in ['test@beispiel.de', 'admin@firma.de']
def registriere_benutzer(daten: Dict[str, Any]) -> bool:
"""
Registriert einen neuen Benutzer in der Datenbank.
"""
try:
# Hier würde die Datenbanklogik stehen
# Passwort hashen, Benutzer speichern, etc.
print(f"Registriere Benutzer: {daten['email']}")
return True
except Exception as e:
print(f"Registrierungsfehler: {e}")
return FalseKomplexe Geschäftsregeln erfordern oft benutzerdefinierte Validierungsfunktionen. Diese sollten klar strukturiert und testbar sein.
def validate_german_postal_code(plz: str) -> List[str]:
"""
Validiert deutsche Postleitzahlen.
Deutsche PLZ bestehen aus genau 5 Ziffern und beginnen mit 0-9.
"""
errors = []
if plz:
if not re.match(r'^\d{5}$', plz):
errors.append('Deutsche Postleitzahl muss aus genau 5 Ziffern bestehen.')
elif plz.startswith('00'):
errors.append('Ungültige Postleitzahl.')
return errors
def validate_iban(iban: str) -> List[str]:
"""
Grundlegende IBAN-Validierung für deutsche IBANs.
"""
errors = []
if iban:
# Leerzeichen entfernen und in Großbuchstaben umwandeln
iban_clean = iban.replace(' ', '').upper()
# Deutsche IBAN prüfen (DE + 20 Zeichen)
if not re.match(r'^DE\d{20}$', iban_clean):
errors.append('Bitte geben Sie eine gültige deutsche IBAN ein.')
else:
# Vereinfachte Prüfsummen-Validierung
if not validate_iban_checksum(iban_clean):
errors.append('IBAN-Prüfsumme ist ungültig.')
return errors
def validate_iban_checksum(iban: str) -> bool:
"""
Validiert die IBAN-Prüfsumme nach dem Standard-Algorithmus.
"""
# IBAN umordnen: die ersten 4 Zeichen ans Ende
rearranged = iban[4:] + iban[:4]
# Buchstaben in Zahlen umwandeln (A=10, B=11, ..., Z=35)
numeric_string = ''
for char in rearranged:
if char.isdigit():
numeric_string += char
else:
numeric_string += str(ord(char) - ord('A') + 10)
# Modulo 97 berechnen
return int(numeric_string) % 97 == 1
def validate_company_tax_id(tax_id: str) -> List[str]:
"""
Validiert deutsche Steuernummern (vereinfacht).
"""
errors = []
if tax_id:
# Format: XX/XXX/XXXXX (Bundesland/Bezirk/Nummer)
if not re.match(r'^\d{2}/\d{3}/\d{5}$', tax_id):
errors.append('Steuernummer muss im Format XX/XXX/XXXXX eingegeben werden.')
return errorsCross-Site Request Forgery (CSRF) ist eine Sicherheitslücke, bei der Angreifer ungewollte Aktionen im Namen eines angemeldeten Benutzers ausführen können. Der Schutz vor CSRF-Angriffen ist in produktiven Webanwendungen unverzichtbar. Flask bietet durch die Flask-WTF Extension elegante Mechanismen für CSRF-Schutz.
CSRF-Angriffe funktionieren, weil Browser automatisch Cookies (einschließlich Session-Cookies) mit jeder Anfrage an eine Domain senden. Ein Angreifer kann eine schädliche Website erstellen, die eine POST-Anfrage an Ihre Anwendung sendet. Wenn ein Benutzer gleichzeitig in Ihrer Anwendung angemeldet ist, wird die Anfrage mit gültigen Session-Daten ausgeführt.
Beispiel eines CSRF-Angriffs: Ein Benutzer ist in einer Banking-Anwendung angemeldet. Er besucht eine schädliche Website, die im Hintergrund ein verstecktes Formular absendet, das Geld auf das Konto des Angreifers überweist. Da der Browser die Session-Cookies automatisch mitsendet, sieht die Bank die Anfrage als legitim an.
CSRF-Token lösen dieses Problem, indem sie bei jeder Formular-Übertragung ein eindeutiges, unvorhersagbares Token erfordern. Dieses Token wird in das Formular eingebettet und bei der Verarbeitung validiert.
from flask import Flask, render_template, request, session, abort
import secrets
import hmac
import hashlib
class CSRFProtection:
"""
Einfache CSRF-Schutz-Implementierung für Bildungszwecke.
In der Praxis sollten Sie Flask-WTF verwenden.
"""
def __init__(self, app, secret_key):
self.secret_key = secret_key.encode('utf-8')
self.app = app
def generate_csrf_token(self):
"""Generiert ein CSRF-Token für die aktuelle Session."""
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_urlsafe(32)
# Token mit Secret Key signieren
message = session['csrf_token'].encode('utf-8')
signature = hmac.new(self.secret_key, message, hashlib.sha256).hexdigest()
return f"{session['csrf_token']}.{signature}"
def validate_csrf_token(self, token):
"""Validiert ein CSRF-Token."""
if not token or '.' not in token:
return False
token_value, signature = token.rsplit('.', 1)
# Session-Token prüfen
if session.get('csrf_token') != token_value:
return False
# Signatur prüfen
expected_signature = hmac.new(
self.secret_key,
token_value.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask-App mit CSRF-Schutz
app = Flask(__name__)
app.secret_key = 'ihr-sehr-geheimer-schluessel'
csrf = CSRFProtection(app, app.secret_key)
@app.template_global()
def csrf_token():
"""Template-Funktion für CSRF-Token-Generierung."""
return csrf.generate_csrf_token()
def csrf_protect():
"""Decorator für CSRF-geschützte Routes."""
def decorator(f):
def decorated_function(*args, **kwargs):
if request.method == 'POST':
token = request.form.get('csrf_token')
if not csrf.validate_csrf_token(token):
abort(403) # Forbidden
return f(*args, **kwargs)
decorated_function.__name__ = f.__name__
return decorated_function
return decorator
@app.route('/geschuetztes-formular', methods=['GET', 'POST'])
@csrf_protect()
def geschuetztes_formular():
if request.method == 'POST':
# Hier ist sicher, dass die Anfrage ein gültiges CSRF-Token hat
name = request.form.get('name')
email = request.form.get('email')
# Verarbeitung der Daten
return f"Daten empfangen: {name}, {email}"
return render_template('geschuetztes_formular.html')CSRF-Token müssen in jedes Formular eingebettet werden, das Daten an den Server sendet.
<!-- templates/geschuetztes_formular.html -->
{% extends "base.html" %}
{% block content %}
<div class="container">
<form method="POST">
<!-- CSRF-Token als verstecktes Feld -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary">Absenden</button>
</form>
</div>
{% endblock %}Flask-WTF bietet eine ausgereifte, produktionsreife CSRF-Schutz-Implementierung, die in echten Anwendungen verwendet werden sollte.
from flask import Flask, render_template, request, flash, redirect, url_for
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, EmailField, PasswordField, SelectField, TextAreaField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo
app = Flask(__name__)
app.config['SECRET_KEY'] = 'ihr-sehr-geheimer-schluessel'
# CSRF-Schutz aktivieren
csrf = CSRFProtect(app)
class KontaktForm(FlaskForm):
"""
Beispiel-Formular mit Flask-WTF.
Die CSRF-Funktionalität ist automatisch eingebaut.
"""
vorname = StringField('Vorname', validators=[
DataRequired(message='Vorname ist erforderlich.'),
Length(min=2, max=50, message='Vorname muss zwischen 2 und 50 Zeichen lang sein.')
])
nachname = StringField('Nachname', validators=[
DataRequired(message='Nachname ist erforderlich.'),
Length(min=2, max=50, message='Nachname muss zwischen 2 und 50 Zeichen lang sein.')
])
email = EmailField('E-Mail-Adresse', validators=[
DataRequired(message='E-Mail-Adresse ist erforderlich.'),
Email(message='Bitte geben Sie eine gültige E-Mail-Adresse ein.')
])
betreff = SelectField('Betreff', choices=[
('', 'Bitte wählen...'),
('allgemein', 'Allgemeine Anfrage'),
('support', 'Technischer Support'),
('vertrieb', 'Vertriebsanfrage'),
('bewerbung', 'Bewerbung')
], validators=[DataRequired(message='Bitte wählen Sie einen Betreff aus.')])
nachricht = TextAreaField('Nachricht', validators=[
DataRequired(message='Nachricht ist erforderlich.'),
Length(min=10, max=1000, message='Nachricht muss zwischen 10 und 1000 Zeichen lang sein.')
])
datenschutz = BooleanField('Datenschutzerklärung', validators=[
DataRequired(message='Sie müssen der Datenschutzerklärung zustimmen.')
])
@app.route('/kontakt', methods=['GET', 'POST'])
def kontakt():
form = KontaktForm()
if form.validate_on_submit():
# CSRF-Token wurde automatisch validiert
# Formulardaten sind verfügbar über form.field.data
kontakt_daten = {
'vorname': form.vorname.data,
'nachname': form.nachname.data,
'email': form.email.data,
'betreff': form.betreff.data,
'nachricht': form.nachricht.data
}
# Kontaktanfrage verarbeiten
if verarbeite_kontaktanfrage(kontakt_daten):
flash('Ihre Nachricht wurde erfolgreich gesendet. Wir melden uns in Kürze bei Ihnen.', 'success')
return redirect(url_for('kontakt'))
else:
flash('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', 'error')
return render_template('kontakt_form.html', form=form)
def verarbeite_kontaktanfrage(daten):
"""Verarbeitet eine Kontaktanfrage."""
try:
# Hier würde die E-Mail-Versendung oder Datenbankoperationen stattfinden
print(f"Kontaktanfrage von {daten['email']}: {daten['betreff']}")
return True
except Exception as e:
print(f"Fehler bei Kontaktanfrage: {e}")
return FalseEffektive Fehlerbehandlung ist entscheidend für eine gute Benutzererfahrung. Benutzer müssen klar verstehen, was schiefgelaufen ist und wie sie das Problem beheben können. Eine durchdachte Fehlerbehandlung umfasst sowohl die Validierung als auch die Präsentation von Fehlermeldungen.
Eine gut strukturierte Fehlerbehandlung unterscheidet zwischen verschiedenen Arten von Fehlern und behandelt sie entsprechend.
from enum import Enum
from typing import Dict, List, Optional
class ErrorType(Enum):
"""Verschiedene Arten von Validierungsfehlern."""
REQUIRED = "required"
FORMAT = "format"
LENGTH = "length"
BUSINESS_RULE = "business_rule"
DATABASE = "database"
class ValidationError:
"""Eine einzelne Validierungsfehlermeldung."""
def __init__(self, field: str, message: str, error_type: ErrorType, value: Optional[str] = None):
self.field = field
self.message = message
self.error_type = error_type
self.value = value
def to_dict(self):
return {
'field': self.field,
'message': self.message,
'type': self.error_type.value,
'value': self.value
}
class FormErrorHandler:
"""Zentrale Klasse für Formular-Fehlerbehandlung."""
def __init__(self):
self.errors: List[ValidationError] = []
def add_error(self, field: str, message: str, error_type: ErrorType, value: Optional[str] = None):
"""Fügt einen Validierungsfehler hinzu."""
self.errors.append(ValidationError(field, message, error_type, value))
def has_errors(self) -> bool:
"""Prüft, ob Fehler vorhanden sind."""
return len(self.errors) > 0
def get_errors_by_field(self) -> Dict[str, List[str]]:
"""Gruppiert Fehlermeldungen nach Feldnamen."""
field_errors = {}
for error in self.errors:
if error.field not in field_errors:
field_errors[error.field] = []
field_errors[error.field].append(error.message)
return field_errors
def get_all_messages(self) -> List[str]:
"""Gibt alle Fehlermeldungen als Liste zurück."""
return [error.message for error in self.errors]
def clear(self):
"""Löscht alle Fehler."""
self.errors.clear()
# Erweiterte Validierungslogik mit Fehlerbehandlung
def validate_registration_form_advanced(form_data: Dict[str, str]) -> FormErrorHandler:
"""
Umfassende Validierung eines Registrierungsformulars.
Zeigt verschiedene Validierungsstrategien und Fehlertypen.
"""
error_handler = FormErrorHandler()
# Vorname validieren
vorname = form_data.get('vorname', '').strip()
if not vorname:
error_handler.add_error('vorname', 'Vorname ist erforderlich.', ErrorType.REQUIRED)
elif len(vorname) < 2:
error_handler.add_error('vorname', 'Vorname muss mindestens 2 Zeichen lang sein.',
ErrorType.LENGTH, vorname)
elif len(vorname) > 50:
error_handler.add_error('vorname', 'Vorname darf maximal 50 Zeichen lang sein.',
ErrorType.LENGTH, vorname)
elif not re.match(r'^[a-zA-ZäöüÄÖÜß\s-]+$', vorname):
error_handler.add_error('vorname', 'Vorname darf nur Buchstaben, Leerzeichen und Bindestriche enthalten.',
ErrorType.FORMAT, vorname)
# E-Mail validieren
email = form_data.get('email', '').strip().lower()
if not email:
error_handler.add_error('email', 'E-Mail-Adresse ist erforderlich.', ErrorType.REQUIRED)
elif not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
error_handler.add_error('email', 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
ErrorType.FORMAT, email)
else:
# Geschäftsregel: E-Mail-Eindeutigkeit prüfen
if email_already_exists(email):
error_handler.add_error('email', 'Diese E-Mail-Adresse ist bereits registriert.',
ErrorType.BUSINESS_RULE, email)
# Passwort validieren
password = form_data.get('password', '')
if not password:
error_handler.add_error('password', 'Passwort ist erforderlich.', ErrorType.REQUIRED)
else:
# Mehrere Passwort-Validierungen
if len(password) < 8:
error_handler.add_error('password', 'Passwort muss mindestens 8 Zeichen lang sein.',
ErrorType.LENGTH)
if not re.search(r'[A-Z]', password):
error_handler.add_error('password', 'Passwort muss mindestens einen Großbuchstaben enthalten.',
ErrorType.FORMAT)
if not re.search(r'[a-z]', password):
error_handler.add_error('password', 'Passwort muss mindestens einen Kleinbuchstaben enthalten.',
ErrorType.FORMAT)
if not re.search(r'\d', password):
error_handler.add_error('password', 'Passwort muss mindestens eine Zahl enthalten.',
ErrorType.FORMAT)
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
error_handler.add_error('password', 'Passwort muss mindestens ein Sonderzeichen enthalten.',
ErrorType.FORMAT)
# Passwort-Bestätigung
password_confirm = form_data.get('password_confirm', '')
if password and password_confirm and password != password_confirm:
error_handler.add_error('password_confirm', 'Passwörter stimmen nicht überein.',
ErrorType.BUSINESS_RULE)
return error_handler
def email_already_exists(email: str) -> bool:
"""Simuliert Datenbankprüfung für E-Mail-Eindeutigkeit."""
# In echter Anwendung: Datenbankabfrage
existing_emails = ['test@beispiel.de', 'admin@firma.de', 'info@firma.de']
return email in existing_emails
@app.route('/erweiterte-registrierung-fehlerbehandlung', methods=['GET', 'POST'])
def erweiterte_registrierung_mit_fehlerbehandlung():
if request.method == 'GET':
return render_template('forms/erweiterte_registrierung_errors.html')
# Formulardaten extrahieren
form_data = {
'vorname': request.form.get('vorname', ''),
'nachname': request.form.get('nachname', ''),
'email': request.form.get('email', ''),
'password': request.form.get('password', ''),
'password_confirm': request.form.get('password_confirm', ''),
'agb_akzeptiert': request.form.get('agb_akzeptiert') == 'on'
}
# Validierung durchführen
error_handler = validate_registration_form_advanced(form_data)
# AGB-Akzeptierung prüfen
if not form_data['agb_akzeptiert']:
error_handler.add_error('agb_akzeptiert',
'Sie müssen den Allgemeinen Geschäftsbedingungen zustimmen.',
ErrorType.BUSINESS_RULE)
if error_handler.has_errors():
# Fehler für Template aufbereiten
field_errors = error_handler.get_errors_by_field()
# Flash-Nachrichten für allgemeine Fehler
for message in error_handler.get_all_messages():
flash(message, 'error')
return render_template('forms/erweiterte_registrierung_errors.html',
form_data=form_data,
field_errors=field_errors)
# Keine Fehler - Registrierung durchführen
try:
if register_user_safe(form_data):
flash('Registrierung erfolgreich! Bitte bestätigen Sie Ihre E-Mail-Adresse.', 'success')
return redirect(url_for('login'))
else:
flash('Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.', 'error')
return render_template('forms/erweiterte_registrierung_errors.html',
form_data=form_data)
except Exception as e:
# Unerwartete Fehler protokollieren
app.logger.error(f"Registrierungsfehler für {form_data.get('email', 'unbekannt')}: {str(e)}")
flash('Ein technischer Fehler ist aufgetreten. Bitte kontaktieren Sie den Support.', 'error')
return render_template('forms/erweiterte_registrierung_errors.html',
form_data=form_data)
def register_user_safe(form_data: Dict[str, str]) -> bool:
"""
Sichere Benutzerregistrierung mit Fehlerbehandlung.
"""
try:
# Hash das Passwort
from werkzeug.security import generate_password_hash
password_hash = generate_password_hash(form_data['password'])
# Benutzer in Datenbank speichern
# In echter Anwendung: SQLAlchemy oder PyODBC
user_data = {
'vorname': form_data['vorname'],
'nachname': form_data['nachname'],
'email': form_data['email'],
'password_hash': password_hash
}
# Simuliere Datenbankoperation
print(f"Registriere Benutzer: {user_data['email']}")
# E-Mail-Bestätigung senden
send_confirmation_email(user_data['email'])
return True
except Exception as e:
# Fehler protokollieren ohne sensible Daten preiszugeben
app.logger.error(f"Datenbankfehler bei Registrierung: {str(e)}")
return False
def send_confirmation_email(email: str):
"""Sendet Bestätigungs-E-Mail."""
# In echter Anwendung: Flask-Mail oder ähnliches
print(f"Bestätigungs-E-Mail gesendet an: {email}")Die Präsentation von Fehlern im Frontend erfordert durchdachte UX-Entscheidungen.
<!-- templates/forms/erweiterte_registrierung_errors.html -->
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">Benutzer-Registrierung</h4>
</div>
<div class="card-body">
<!-- Allgemeine Fehlermeldungen -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert-container mb-3">
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="POST" class="needs-validation" novalidate>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="vorname" class="form-label">Vorname *</label>
<input type="text"
class="form-control{% if field_errors.get('vorname') %} is-invalid{% endif %}"
id="vorname"
name="vorname"
value="{{ form_data.get('vorname', '') }}"
required>
{% if field_errors.get('vorname') %}
<div class="invalid-feedback">
{% for error in field_errors['vorname'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="nachname" class="form-label">Nachname *</label>
<input type="text"
class="form-control{% if field_errors.get('nachname') %} is-invalid{% endif %}"
id="nachname"
name="nachname"
value="{{ form_data.get('nachname', '') }}"
required>
{% if field_errors.get('nachname') %}
<div class="invalid-feedback">
{% for error in field_errors['nachname'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-Mail-Adresse *</label>
<input type="email"
class="form-control{% if field_errors.get('email') %} is-invalid{% endif %}"
id="email"
name="email"
value="{{ form_data.get('email', '') }}"
required>
{% if field_errors.get('email') %}
<div class="invalid-feedback">
{% for error in field_errors['email'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="password" class="form-label">Passwort *</label>
<input type="password"
class="form-control{% if field_errors.get('password') %} is-invalid{% endif %}"
id="password"
name="password"
required>
{% if field_errors.get('password') %}
<div class="invalid-feedback">
{% for error in field_errors['password'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% else %}
<div class="form-text">
Mindestens 8 Zeichen mit Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen.
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="password_confirm" class="form-label">Passwort bestätigen *</label>
<input type="password"
class="form-control{% if field_errors.get('password_confirm') %} is-invalid{% endif %}"
id="password_confirm"
name="password_confirm"
required>
{% if field_errors.get('password_confirm') %}
<div class="invalid-feedback">
{% for error in field_errors['password_confirm'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3 form-check">
<input class="form-check-input{% if field_errors.get('agb_akzeptiert') %} is-invalid{% endif %}"
type="checkbox"
id="agb_akzeptiert"
name="agb_akzeptiert"
{% if form_data.get('agb_akzeptiert') %}checked{% endif %}
required>
<label class="form-check-label" for="agb_akzeptiert">
Ich stimme den <a href="{{ url_for('agb') }}" target="_blank">Allgemeinen Geschäftsbedingungen</a> zu *
</label>
{% if field_errors.get('agb_akzeptiert') %}
<div class="invalid-feedback">
{% for error in field_errors['agb_akzeptiert'] %}
{{ error }}{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
Registrierung absenden
</button>
</div>
<div class="mt-3 text-center">
<small class="text-muted">
Bereits registriert? <a href="{{ url_for('login') }}">Hier anmelden</a>
</small>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// Clientseitige Validierung zur Verbesserung der Benutzererfahrung
document.addEventListener('DOMContentLoaded', function() {
// Passwort-Bestätigung in Echtzeit prüfen
const passwordField = document.getElementById('password');
const confirmField = document.getElementById('password_confirm');
function checkPasswordMatch() {
if (confirmField.value && passwordField.value !== confirmField.value) {
confirmField.setCustomValidity('Passwörter stimmen nicht überein');
} else {
confirmField.setCustomValidity('');
}
}
passwordField.addEventListener('input', checkPasswordMatch);
confirmField.addEventListener('input', checkPasswordMatch);
// Bootstrap-Validierung aktivieren
const forms = document.querySelectorAll('.needs-validation');
forms.forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
});
});
</script>
{% endblock %}Diese umfassende Behandlung von Formularen und Validierung bildet die Grundlage für sichere, benutzerfreundliche Webanwendungen. Die Kombination aus clientseitiger und serverseitiger Validierung, CSRF-Schutz und strukturierter Fehlerbehandlung gewährleistet sowohl Sicherheit als auch Benutzerfreundlichkeit.