8 Formulare und Validierung

8.1 HTML-Formulare erstellen

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.

8.1.1 Grundlagen der HTML-Formular-Struktur

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.

8.1.2 Eingabefeld-Typen und deren Verwendung

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 %}

8.1.3 Flask-Route für Formularverarbeitung

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 False

8.2 Datenvalidierung

Datenvalidierung 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.

8.2.1 Serverseitige Validierung verstehen

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 False

8.2.2 Benutzerdefinierte Validierungsfunktionen

Komplexe 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 errors

8.3 CSRF-Schutz

Cross-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.

8.3.1 CSRF-Angriffe verstehen

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.

8.3.2 CSRF-Token-Implementierung

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')

8.3.3 Template-Integration für CSRF-Schutz

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 %}

8.3.4 Flask-WTF für professionellen CSRF-Schutz

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 False

8.4 Fehlerbehandlung bei Formularen

Effektive 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.

8.4.1 Strukturierte Fehlerbehandlung

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}")

8.4.2 Template für Fehleranzeige

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.