Login y registro de usuarios en Flask desde cero
Un sistema de autenticación completo en Flask: registro con contraseña cifrada, inicio de sesión, sesión persistente y rutas protegidas. Sin librerías mágicas, entendiendo cada pieza.
Casi toda app real necesita usuarios: alguien se registra, inicia sesión, y ve cosas que otros no. Aquí lo construimos de principio a fin en Flask, entendiendo cada parte en vez de copiar una extensión que hace magia. Al terminar tendrás registro, login, logout y rutas que solo ven los usuarios autenticados.
Damos por hecho que ya tienes una app Flask con SQLite andando (si no, empieza por el tutorial de tu primera app Flask con SQLite).
La regla de oro: nunca guardes contraseñas en texto plano
Antes de una sola línea: las contraseñas jamás se guardan tal cual en la base de datos. Se guarda un hash —una huella irreversible— de modo que ni tú, ni nadie que acceda a la base, pueda leer la contraseña real. Para eso usamos las utilidades que Flask ya trae a través de Werkzeug. Nada de inventar cifrado propio.
La tabla de usuarios
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
clave_hash TEXT NOT NULL
);
Fíjate en UNIQUE en el email: la base rechaza dos usuarios con el mismo correo, una validación gratis a nivel de datos.
Registro
Al registrarse, ciframos la contraseña antes de guardarla:
from flask import Flask, request, redirect, url_for, session, render_template, flash
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
app = Flask(__name__)
app.secret_key = "cambia-esto-por-una-clave-larga-y-secreta"
def get_db():
conn = sqlite3.connect("usuarios.db")
conn.row_factory = sqlite3.Row
return conn
@app.route("/registro", methods=["GET", "POST"])
def registro():
if request.method == "POST":
email = request.form["email"].strip().lower()
clave = request.form["clave"]
db = get_db()
try:
db.execute(
"INSERT INTO usuarios (email, clave_hash) VALUES (?, ?)",
(email, generate_password_hash(clave)),
)
db.commit()
except sqlite3.IntegrityError:
flash("Ese correo ya está registrado.")
return redirect(url_for("registro"))
finally:
db.close()
return redirect(url_for("login"))
return render_template("registro.html")
La pieza clave es generate_password_hash(clave): convierte la contraseña en un hash seguro antes de que toque la base. El try/except IntegrityError atrapa el caso de email duplicado que la restricción UNIQUE provoca.
Login
Al iniciar sesión, comparamos la contraseña recibida contra el hash guardado:
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form["email"].strip().lower()
clave = request.form["clave"]
db = get_db()
usuario = db.execute(
"SELECT * FROM usuarios WHERE email = ?", (email,)
).fetchone()
db.close()
if usuario and check_password_hash(usuario["clave_hash"], clave):
session["usuario_id"] = usuario["id"]
return redirect(url_for("panel"))
flash("Correo o contraseña incorrectos.")
return render_template("login.html")
check_password_hash hace la comparación segura entre la contraseña escrita y el hash guardado. Si coincide, guardamos el id del usuario en session, que es lo que mantiene la sesión abierta entre peticiones.
Un detalle de seguridad importante: el mensaje de error dice “correo o contraseña incorrectos”, sin especificar cuál falló. Eso evita que alguien averigüe qué correos están registrados probando direcciones.
Proteger rutas
Ahora, las páginas que solo deben ver los usuarios logueados. Creamos un pequeño verificador reutilizable:
from functools import wraps
def requiere_login(f):
@wraps(f)
def decorada(*args, **kwargs):
if "usuario_id" not in session:
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorada
@app.route("/panel")
@requiere_login
def panel():
return render_template("panel.html")
Ese decorador @requiere_login lo pones encima de cualquier ruta que quieras proteger. Si no hay sesión activa, redirige al login. Es el mismo patrón que usan las extensiones grandes por dentro, pero aquí lo ves entero y sin caja negra.
Logout
Cerrar sesión es simplemente vaciar la sesión:
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("login"))
Lo que no debes olvidar en producción
Dos cosas antes de llevar esto a un servidor real. Primero, el secret_key: en el ejemplo está escrito a la vista, pero en producción debe ser una cadena larga y aleatoria, cargada desde una variable de entorno, nunca en el código. Es lo que firma las sesiones; si se filtra, cualquiera puede falsificarlas.
Segundo, sirve siempre sobre HTTPS. Sin cifrado en tránsito, las contraseñas viajan legibles por la red por muy bien que las cifres en la base.
Con esto tienes autenticación real y entendida. A partir de aquí, todo lo demás —recuperar contraseña, roles de usuario, “recordarme”— son extensiones de estas mismas piezas.
¿Vas a añadir roles, verificación por correo o “recordarme”? Escríbeme desde contacto y lo cubrimos en un próximo artículo.