📝 Modulo 5: Form e Input Utente
Impara a raccogliere, validare e processare dati dagli utenti
5 di 8
45 minuti
Intermedio
🎯 Perché i Form Sono Importanti?
I form HTML sono il ponte tra l’utente e la tua applicazione. Permettono di:
Raccogliere Dati
Nome, email, password, preferenze
Autenticazione
Login e registrazione utenti
Interazione
Commenti, feedback, contatti
Transazioni
E-commerce, pagamenti, ordini
💡 Ciclo di Vita di un Form
Un form in Flask segue questo percorso:
- GET Request: L’utente visita la pagina con il form vuoto
- Compilazione: L’utente riempie i campi e clicca “Invia”
- POST Request: I dati vengono inviati al server
- Validazione: Flask controlla che i dati siano corretti
- Processing: I dati vengono elaborati e salvati
- Response: Mostra un risultato o reindirizza
🚀 Il Tuo Primo Form in Flask
Iniziamo con un semplice form di contatto.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Contattaci</title>
</head>
<body>
<h1>Contattaci</h1>
<form method="POST" action="{{ url_for('contatto') }}">
<div>
<label for="nome">Nome:</label>
<input type="text" id="nome" name="nome" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="messaggio">Messaggio:</label>
<textarea id="messaggio" name="messaggio" rows="4" required></textarea>
</div>
<button type="submit">Invia</button>
</form>
</body>
</html>
from flask import Flask, render_template, request
app = Flask(__name__)
# Route per mostrare il form (GET)
@app.route('/contatto', methods=['GET'])
def mostra_form():
return render_template('contatto.html')
# Route per processare il form (POST)
@app.route('/contatto', methods=['POST'])
def processa_form():
# Ottieni i dati dal form
nome = request.form.get('nome')
email = request.form.get('email')
messaggio = request.form.get('messaggio')
# Qui potresti salvare nel database o inviare email
print(f"Nuovo messaggio da {nome} ({email}): {messaggio}")
return f"Grazie {nome}! Il tuo messaggio è stato ricevuto."
if __name__ == '__main__':
app.run(debug=True)
🔍 Spiegazione del Codice
methods=['GET', 'POST']: Specifica che la route accetta entrambi i metodirequest.form.get('nome'): Recupera il valore del campo “nome” dal formaction="{{ url_for('contatto') }}": Invia il form alla stessa routerequired: Attributo HTML5 per validazione lato client
📡 Metodi HTTP: GET vs POST
Comprendere la differenza tra GET e POST è fondamentale per i form.
| Metodo | Scopo | Dati | Sicurezza | Quando Usare |
|---|---|---|---|---|
| GET | Ottenere dati | Nell’URL (visibili) | ⚠️ Bassa | Ricerca, filtri, paginazione |
| POST | Inviare dati | Nel corpo (nascosti) | ✅ Alta | Login, registrazione, pagamenti |
🎯 Best Practice
SEMPRE usare POST per:
- Password e dati sensibili
- Operazioni che modificano dati (CRUD)
- File upload
- Transazioni finanziarie
Usare GET per:
- Filtrare risultati di ricerca
- Paginazione
- Condividere link con parametri
🎨 Form Avanzato con Tutti i Campi
Esempio di Form Completo
<form method="POST" action="{{ url_for('registrazione') }}">
<!-- Campi di testo -->
<input type="text" name="nome_completo">
<input type="email" name="email">
<input type="tel" name="telefono">
<input type="date" name="data_nascita">
<!-- Select -->
<select name="genere">
<option value="maschio">Maschio</option>
<option value="femmina">Femmina</option>
</select>
<!-- Checkbox multiple -->
<input type="checkbox" name="interessi" value="tecnologia">
<input type="checkbox" name="interessi" value="sport">
<!-- Radio buttons -->
<input type="radio" name="newsletter" value="si">
<input type="radio" name="newsletter" value="no">
<!-- Textarea -->
<textarea name="messaggio"></textarea>
<!-- File upload -->
<input type="file" name="foto_profilo">
<button type="submit">Registrati</button>
</form>
🔐 Validazione dei Dati
La validazione è CRUCIALE per la sicurezza e l’integrità dei dati.
Immediata ma facile da bypassare:
<!-- HTML5 Validation -->
<input type="text" required>
<input type="email" required>
<input type="number" min="1" max="100">
<input type="password" minlength="8">
<input type="text" pattern="[A-Za-z]{3,}">
Obbligatoria per sicurezza:
from flask import Flask, request, render_template, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'chiave-segreta' # Necessaria per i flash messages
@app.route('/registrazione', methods=['GET', 'POST'])
def registrazione():
if request.method == 'POST':
# Recupera i dati
nome = request.form.get('nome', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# Lista errori
errori = []
# Validazione Nome
if not nome:
errori.append('Il nome è obbligatorio')
elif len(nome) < 2:
errori.append('Il nome deve essere di almeno 2 caratteri')
# Validazione Email
if not email:
errori.append('L\'email è obbligatoria')
elif '@' not in email or '.' not in email:
errori.append('Email non valida')
# Validazione Password
if not password:
errori.append('La password è obbligatoria')
elif len(password) < 8:
errori.append('La password deve essere di almeno 8 caratteri')
# Se ci sono errori, mostra form con errori
if errori:
for errore in errori:
flash(errore, 'error')
return render_template('registrazione.html')
# Se tutto ok, procedi
flash('Registrazione completata con successo!', 'success')
return redirect(url_for('home'))
# Se è una GET, mostra il form vuoto
return render_template('registrazione.html')
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Registrazione</title>
</head>
<body>
<!-- Mostra messaggi flash -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<!-- form fields here -->
</form>
</body>
</html>
💬 Flash Messages - Feedback per l'Utente
I flash messages sono il modo migliore per comunicare con l'utente dopo un'azione.
from flask import flash
# Messaggi di successo
flash('Login effettuato con successo!', 'success')
flash('Prodotto aggiunto al carrello.', 'info')
flash('Email inviata con successo.', 'success')
# Messaggi di errore
flash('Password errata.', 'error')
flash('Email già registrata.', 'warning')
flash('Compila tutti i campi obbligatori.', 'error')
# Messaggi informativi
flash('Controlla la tua email per confermare.', 'info')
flash('La sessione scadrà tra 5 minuti.', 'warning')
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>{% block title %}Sito Flask{% endblock %}</title>
<style>
.flash {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
border-left: 5px solid;
}
.flash.success {
background-color: #d1fae5;
border-color: #10b981;
color: #065f46;
}
.flash.error {
background-color: #fee2e2;
border-color: #ef4444;
color: #991b1b;
}
.flash.warning {
background-color: #fef3c7;
border-color: #f59e0b;
color: #92400e;
}
.flash.info {
background-color: #dbeafe;
border-color: #3b82f6;
color: #1e40af;
}
</style>
</head>
<body>
<!-- Flash Messages Container -->
<div class="flash-container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% block content %}{% endblock %}
</body>
</html>
🛡️ Validazione Sicura con Flask-WTF
Per una validazione robusta e sicura, Flask-WTF è la soluzione professionale:
# Installa Flask-WTF pip install flask-wtf
from flask import Flask, render_template, redirect, url_for, flash, session
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, EmailField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from flask_wtf.csrf import CSRFProtect
import re
app = Flask(__name__)
app.secret_key = 'tua-chiave-segreta-molto-lunga'
# Attiva protezione CSRF
csrf = CSRFProtect(app)
# Validatore personalizzato per sicurezza password
def password_complexity(form, field):
password = field.data
if not re.search(r'[A-Z]', password):
raise ValidationError('La password deve contenere almeno una lettera maiuscola')
if not re.search(r'[a-z]', password):
raise ValidationError('La password deve contenere almeno una lettera minuscola')
if not re.search(r'\d', password):
raise ValidationError('La password deve contenere almeno un numero')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
raise ValidationError('La password deve contenere almeno un carattere speciale')
# Form di Registrazione Sicuro
class RegistrazioneSicuraForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(message='L\'username è obbligatorio'),
Length(min=3, max=20, message='L\'username deve essere tra 3 e 20 caratteri'),
])
email = EmailField('Email', validators=[
DataRequired(message='L\'email è obbligatoria'),
Email(message='Inserisci un\'email valida'),
])
password = PasswordField('Password', validators=[
DataRequired(message='La password è obbligatoria'),
Length(min=8, message='La password deve essere di almeno 8 caratteri'),
password_complexity # Validatore personalizzato
])
conferma_password = PasswordField('Conferma Password', validators=[
DataRequired(message='Devi confermare la password'),
EqualTo('password', message='Le password non coincidono')
])
accetta_termini = BooleanField('Accetto i termini e condizioni', validators=[
DataRequired(message='Devi accettare i termini e condizioni')
])
submit = SubmitField('Registrati')
# Form di Login
class LoginForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(message='Inserisci il tuo username')
])
password = PasswordField('Password', validators=[
DataRequired(message='Inserisci la tua password')
])
remember_me = BooleanField('Ricordami')
submit = SubmitField('Accedi')
from functools import wraps
# Decorator per proteggere le route
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('Devi fare il login per accedere a questa pagina', 'warning')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# Route di registrazione
@app.route('/registrazione-sicura', methods=['GET', 'POST'])
def registrazione_sicura():
form = RegistrazioneSicuraForm()
if form.validate_on_submit():
# Qui salveresti l'utente nel database
# Per ora simuliamo con la sessione
# Crittografa la password prima di salvarla (in produzione!)
# from werkzeug.security import generate_password_hash
# hashed_password = generate_password_hash(form.password.data)
# Salva in sessione (temporaneo)
session['user_id'] = 1
session['username'] = form.username.data
session['email'] = form.email.data
session['logged_in'] = True
flash('🎉 Registrazione completata con successo!', 'success')
return redirect(url_for('dashboard'))
return render_template('registrazione_sicura.html', form=form)
# Route di login
@app.route('/login-sicuro', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# Qui verificheresti le credenziali dal database
# Per ora simuliamo
if form.username.data == 'admin' and form.password.data == 'Password123!':
session['user_id'] = 1
session['username'] = form.username.data
session['logged_in'] = True
if form.remember_me.data:
# Imposta cookie permanente
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
flash('✅ Login effettuato con successo!', 'success')
# Reindirizza alla pagina richiesta o alla dashboard
next_page = request.args.get('next')
return redirect(next_page or url_for('dashboard'))
else:
flash('❌ Credenziali non valide', 'error')
return render_template('login_sicuro.html', form=form)
# Route protetta
@app.route('/dashboard')
@login_required # ⭐⭐⭐ SOLO UTENTI LOGGATI ⭐⭐⭐
def dashboard():
return render_template('dashboard.html', username=session['username'])
# Logout
@app.route('/logout')
def logout():
session.clear()
flash('👋 Logout effettuato', 'info')
return redirect(url_for('home'))
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Registrazione Sicura</title>
<style>
.form-container {
max-width: 600px;
margin: 50px auto;
padding: 30px;
background: #f8fafc;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.form-title {
color: #7c3aed;
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 6px;
font-size: 16px;
transition: all 0.3s;
}
.form-control:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.form-control.is-invalid {
border-color: #ef4444;
}
.invalid-feedback {
color: #ef4444;
font-size: 14px;
margin-top: 5px;
display: block;
}
.password-requirements {
background: #f0f9ff;
border-left: 4px solid #0ea5e9;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.password-requirements ul {
margin: 0;
padding-left: 20px;
}
.password-requirements li {
margin-bottom: 5px;
}
.btn-primary {
width: 100%;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
padding: 14px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.login-link {
text-align: center;
margin-top: 20px;
color: #6b7280;
}
.login-link a {
color: #7c3aed;
text-decoration: none;
font-weight: 600;
}
</style>
</head>
<body>
<div class="form-container">
<h1 class="form-title">🔐 Registrazione Sicura</h1>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('registrazione_sicura') }}">
{{ form.hidden_tag() }} <!-- CSRF Token -->
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
<div class="password-requirements">
<strong>Requisiti password:</strong>
<ul>
<li>Almeno 8 caratteri</li>
<li>Almeno una lettera maiuscola</li>
<li>Almeno una lettera minuscola</li>
<li>Almeno un numero</li>
<li>Almeno un carattere speciale</li>
</ul>
</div>
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.conferma_password.label }}
{{ form.conferma_password(class="form-control") }}
{% if form.conferma_password.errors %}
{% for error in form.conferma_password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.accetta_termini(class="form-check-input") }}
{{ form.accetta_termini.label }}
{% if form.accetta_termini.errors %}
{% for error in form.accetta_termini.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
{{ form.submit(class="btn-primary") }}
</form>
<div class="login-link">
Già registrato? <a href="{{ url_for('login') }}">Accedi qui</a>
</div>
</div>
</body>
</html>
🔒 Best Practice di Sicurezza
Sempre usare hashing (bcrypt, werkzeug)
from werkzeug.security import generate_password_hash, check_password_hash # Hash della password hashed = generate_password_hash(password) # Verifica check_password_hash(hashed, password)
Limita tentativi di login
# Installa Flask-Limiter
pip install Flask-Limiter
# Configura
limiter = Limiter(app)
@app.route('/login')
@limiter.limit("5 per minute")
def login():
...
Sessione sicura con timeout
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
PERMANENT_SESSION_LIFETIME=1800 # 30 min
)
👁️ Anteprima Form Sicuro
🛡️ Sistema di Login Sicuro
✅ Funzionalità Implementate:
CSRF Protection
Validazione Server
Password Complesse
Login Required
Session Security
Rate Limiting
🔐 Codice di Esempio:
@login_required # ⭐ Solo loggati ⭐
def dashboard():
return render_template('dashboard.html')
📁 Upload di File
Flask permette di gestire facilmente l'upload di file.
from flask import Flask, request, render_template
import os
from werkzeug.utils import secure_filename
app = Flask(__name__)
# Configurazione upload
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'txt'}
# Controlla estensioni permesse
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# Controlla se il file è presente
if 'file' not in request.files:
flash('Nessun file selezionato', 'error')
return redirect(request.url)
file = request.files['file']
# Se l'utente non seleziona file
if file.filename == '':
flash('Nessun file selezionato', 'error')
return redirect(request.url)
# Controlla estensione e salva
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
flash('File caricato con successo!', 'success')
return redirect(url_for('upload_file'))
else:
flash('Tipo di file non consentito', 'error')
return render_template('upload.html')
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="file">Seleziona file:</label>
<input type="file" id="file" name="file" required>
<small>Formati consentiti: PNG, JPG, GIF, PDF, TXT (max 16MB)</small>
</div>
<div class="form-group">
<label for="descrizione">Descrizione:</label>
<input type="text" id="descrizione" name="descrizione">
</div>
<button type="submit" class="btn">Carica File</button>
</form>
⚠️ IMPORTANTE: Sicurezza File Upload
- SEMPRE usare
secure_filename()per prevenire path traversal - Limitare le dimensioni dei file con
MAX_CONTENT_LENGTH - Validare le estensioni dei file
- Non salvare file nella root dell'applicazione
- Considerare il salvataggio su servizi esterni (AWS S3, Cloudinary)
🛡️ Protezione CSRF
CSRF (Cross-Site Request Forgery) è un attacco che forza un utente ad eseguire azioni non volute.
❌ Senza CSRF Protection
Un malintenzionato potrebbe creare un form che, quando visitato da un utente loggato, esegue azioni al posto suo (trasferimenti, cambi password, ecc.).
# Installa la libreria pip install flask-wtf
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'chiave-molto-segreta-e-complessa'
# Abilita CSRF Protection
csrf = CSRFProtect(app)
# Definisci il form
class RegistrazioneForm(FlaskForm):
nome = StringField('Nome', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8, message='La password deve essere di almeno 8 caratteri')
])
submit = SubmitField('Registrati')
<form method="POST">
{{ form.hidden_tag() }} <!-- CSRF Token -->
<div class="form-group">
{{ form.nome.label }}
{{ form.nome(class="form-control") }}
{% if form.nome.errors %}
<div class="error">
{% for error in form.nome.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="error">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
💪 Esercizi Pratici
✅ Modulo 5 Completato!
Ora sai gestire l'input degli utenti in modo sicuro e professionale. Pronto per salvare i dati in modo permanente?
🎯 Cosa Aspettarti nel Prossimo Modulo
SQLite con Flask
Database leggero integrato
SQLAlchemy ORM
Gestione database con Python
CRUD Completo
Create, Read, Update, Delete