Formulir adalah gerbang utama data masuk ke aplikasi web: pendaftaran, login, checkout. Sayangnya, gerbang ini terbuka untuk semua orang — termasuk yang mengirim data jahat. HTML punya atribut validasi seperti required, tetapi atribut tersebut hidup di browser dan bisa dimatikan dalam hitungan detik oleh siapa pun. Pada chapter ini kita akan membangun alur formulir lengkap di Flask: menampilkan form, memvalidasi di server, menampilkan kembali pesan error, dan melindungi form dengan token CSRF.
Alur Formulir: GET lalu POST #
Pola standar satu route untuk dua method sudah kita kenal pada chapter Routing dan Request Handling:
- Browser membuka
/daftardenganGET— server merender form kosong. - Pengguna mengisi lalu menekan tombol kirim — browser mengirim
POSTdengan data form. - Server memvalidasi; bila gagal, form dirender ulang beserta pesan error; bila sukses, server memproses data.
Berikut template templates/form.html yang dipakai sepanjang chapter ini:
{% if errors %}
<ul class="errors">
{% for pesan in errors.values() %}
<li>{{ pesan }}</li>
{% endfor %}
</ul>
{% endif %}
<form method="post" action="/daftar">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<p>Nama: <input name="nama" value="{{ nilai.nama }}"></p>
<p>Email: <input name="email" value="{{ nilai.email }}"></p>
<p>Umur: <input name="umur" value="{{ nilai.umur }}"></p>
<button type="submit">Daftar</button>
</form>Dua detail penting pada template di atas: setiap input mempertahankan value="{{ nilai.nama }}" agar isian pengguna tidak hilang saat form dirender ulang karena error, dan ada satu input tersembunyi bernama csrf_token yang tugasnya kita bahas di bagian CSRF.
Validasi di Sisi Server #
Fungsi validasi murni — tanpa Flask sama sekali — paling mudah diuji dan dipakai ulang. Pola yang dipakai: kembalikan dictionary kosong bila semua lolos, atau dictionary berisi pesan error per field:
def validasi_pendaftaran(nama, email, umur):
errors = {}
if len(nama) < 3:
errors["nama"] = "Nama minimal 3 karakter."
if "@" not in email or "." not in email.split("@")[-1]:
errors["email"] = "Email tidak valid."
if not umur.isdigit():
errors["umur"] = "Umur harus berupa angka."
elif not 13 <= int(umur) <= 120:
errors["umur"] = "Umur harus di antara 13 dan 120."
return errors
print("validasi murni:", validasi_pendaftaran("Siti", "[email protected]", "17"))
print("validasi error:", validasi_pendaftaran("Si", "salah", "tujuh belas"))Output yang Dihasilkan:
validasi murni: {}
validasi error: {'nama': 'Nama minimal 3 karakter.', 'email': 'Email tidak valid.', 'umur': 'Umur harus berupa angka.'}Perhatikan urutan pemeriksaan umur: isdigit() diperiksa lebih dulu karena int(umur) akan melempar ValueError bila isinya bukan angka — pola guard yang sama dengan materi Exception Handling.
Menangani Form di Flask #
Sekarang fungsi validasi dipasang ke route. Token CSRF dibuat saat GET, disimpan di session, dan dicocokkan saat POST:
import secrets
from flask import Flask, render_template, request, session
app = Flask(__name__)
app.secret_key = "kunci-rahasia-contoh"
@app.route("/daftar", methods=["GET", "POST"])
def daftar():
if request.method == "POST":
if request.form.get("csrf_token") != session.get("csrf_token"):
return "Token CSRF tidak valid!", 400
nilai = {
"nama": request.form.get("nama", "").strip(),
"email": request.form.get("email", "").strip(),
"umur": request.form.get("umur", "").strip(),
}
errors = validasi_pendaftaran(nilai["nama"], nilai["email"], nilai["umur"])
if errors:
return (
render_template("form.html", errors=errors, nilai=nilai,
csrf_token=session["csrf_token"]),
422,
)
session.pop("csrf_token", None) # token sekali pakai
return f"Selamat bergabung, {nilai['nama']}!"
token = secrets.token_hex(16)
session["csrf_token"] = token
return render_template("form.html", errors={},
nilai={"nama": "", "email": "", "umur": ""}, csrf_token=token)Karena seluruh interaksi melibatkan session, pengujian paling akurat adalah lewat test_client() yang otomatis menyimpan cookie session antar request. Token CSRF diambil dari HTML form menggunakan re:
import re
client = app.test_client()
print("GET form pertama :", client.get("/daftar").status_code)
print("POST tanpa CSRF :",
client.post("/daftar", data={"nama": "Budi Santoso",
"email": "[email protected]", "umur": "20"}).status_code)
html = client.get("/daftar").text
token = re.search(r'name="csrf_token" value="([0-9a-f]+)"', html).group(1)
r = client.post("/daftar", data={"nama": "Bu", "email": "budi-example",
"umur": "sembilan", "csrf_token": token})
print("POST data invalid :", r.status_code, "|", re.findall(r"<li>(.+?)</li>", r.text))
r = client.post("/daftar", data={"nama": "Budi Santoso", "email": "[email protected]",
"umur": "20", "csrf_token": token})
print("POST data valid :", r.status_code, "|", r.text)
r = client.post("/daftar", data={"nama": "Budi Santoso", "email": "[email protected]",
"umur": "20", "csrf_token": token})
print("Token dipakai lagi :", r.status_code, "|", r.text)Output yang Dihasilkan:
GET form pertama : 200
POST tanpa CSRF : 400
POST data invalid : 422 | ['Nama minimal 3 karakter.', 'Email tidak valid.', 'Umur harus berupa angka.']
POST data valid : 200 | Selamat bergabung, Budi Santoso!
Token dipakai lagi : 400 | Token CSRF tidak valid!Lima baris output itu merangkum seluruh siklus: POST tanpa token ditolak (400), data tidak valid memicu render ulang berisi pesan error (422), data valid diterima (200), dan token yang sudah dipakai tidak bisa dipakai ulang — mencegah replay attack.
Mengapa CSRF perlu? Bayangkan pengguna yang sedang login di situs kita membuka situs jahat di tab lain; situs jahat itu bisa mengirim form yang menargetkan /daftar milik kita tanpa sepengetahuan pengguna. Token acak yang tidak bisa ditebak situs lain mematahkan skenario tersebut.
WTForms dan Flask-WTF #
Pola errors dict di atas bekerja, tetapi deklaratif lebih rapi untuk banyak form. Extension Flask-WTF menyediakan class form berbasis WTForms — cuplikan berikut hanya ilustrasi bentuknya:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import Length, Email
class FormPendaftaran(FlaskForm):
nama = StringField("Nama", [Length(min=3, message="Nama minimal 3 karakter.")])
email = StringField("Email", [Email(message="Email tidak valid.")])Dengan Flask-WTF, validasi berjalan lewat form.validate_on_submit(), render error lewat form.nama.errors, dan token CSRF sudah dibuat otomatis pada {{ form.hidden_tag() }} — tiga pekerjaan yang barusan kita tulis manual, selesai dalam satu deklarasi.