¿Qué Vamos a Crear?
Un sistema completo de autenticación que incluye registro de usuarios, login, logout, sesiones, encriptación de contraseñas y protección de rutas. Todo con Flask y SQLite.
Instalación de Dependencias
pip install flask
pip install flask-sqlalchemy
pip install flask-login
pip install werkzeug # Para hash de contraseñasEstructura del Proyecto
sistema_login/
├── app.py
├── models.py
├── forms.py
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ ├── registro.html
│ ├── dashboard.html
│ └── perfil.html
└── static/
└── css/
└── style.cssModelos de Base de Datos (models.py)
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
db = SQLAlchemy()
class Usuario(UserMixin, db.Model):
__tablename__ = 'usuarios'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(200), nullable=False)
nombre = db.Column(db.String(100))
fecha_registro = db.Column(db.DateTime, default=datetime.utcnow)
ultimo_acceso = db.Column(db.DateTime)
activo = db.Column(db.Boolean, default=True)
def set_password(self, password):
"""Hashear la contraseña"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Verificar contraseña"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<Usuario {self.username}>'Aplicación Principal (app.py)
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from models import db, Usuario
from datetime import datetime
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'tu-clave-secreta-super-segura-cambiar-en-produccion'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///usuarios.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Inicializar extensiones
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message = 'Por favor inicia sesión para acceder a esta página'
@login_manager.user_loader
def load_user(user_id):
return Usuario.query.get(int(user_id))
# Crear tablas
with app.app_context():
db.create_all()
# Rutas
@app.route('/')
def index():
return render_template('index.html')
@app.route('/registro', methods=['GET', 'POST'])
def registro():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
password2 = request.form.get('password2')
nombre = request.form.get('nombre')
# Validaciones
if not username or not email or not password:
flash('Todos los campos son requeridos', 'danger')
return redirect(url_for('registro'))
if password != password2:
flash('Las contraseñas no coinciden', 'danger')
return redirect(url_for('registro'))
if len(password) < 6:
flash('La contraseña debe tener al menos 6 caracteres', 'danger')
return redirect(url_for('registro'))
# Verificar si usuario ya existe
usuario_existente = Usuario.query.filter(
(Usuario.username == username) | (Usuario.email == email)
).first()
if usuario_existente:
flash('El usuario o email ya existe', 'danger')
return redirect(url_for('registro'))
# Crear nuevo usuario
nuevo_usuario = Usuario(
username=username,
email=email,
nombre=nombre
)
nuevo_usuario.set_password(password)
try:
db.session.add(nuevo_usuario)
db.session.commit()
flash('Registro exitoso! Ahora puedes iniciar sesión', 'success')
return redirect(url_for('login'))
except Exception as e:
db.session.rollback()
flash('Error al registrar usuario', 'danger')
return redirect(url_for('registro'))
return render_template('registro.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
remember = request.form.get('remember', False)
if not username or not password:
flash('Usuario y contraseña requeridos', 'danger')
return redirect(url_for('login'))
# Buscar usuario
usuario = Usuario.query.filter_by(username=username).first()
if usuario and usuario.check_password(password):
# Actualizar último acceso
usuario.ultimo_acceso = datetime.utcnow()
db.session.commit()
# Iniciar sesión
login_user(usuario, remember=remember)
# Redireccionar a página solicitada o dashboard
next_page = request.args.get('next')
flash(f'Bienvenido {usuario.username}!', 'success')
return redirect(next_page or url_for('dashboard'))
else:
flash('Usuario o contraseña incorrectos', 'danger')
return redirect(url_for('login'))
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('Sesión cerrada exitosamente', 'info')
return redirect(url_for('index'))
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
@app.route('/perfil')
@login_required
def perfil():
return render_template('perfil.html', usuario=current_user)
@app.route('/perfil/editar', methods=['POST'])
@login_required
def editar_perfil():
nombre = request.form.get('nombre')
email = request.form.get('email')
if email and email != current_user.email:
# Verificar que email no exista
existente = Usuario.query.filter_by(email=email).first()
if existente:
flash('El email ya está en uso', 'danger')
return redirect(url_for('perfil'))
current_user.email = email
if nombre:
current_user.nombre = nombre
try:
db.session.commit()
flash('Perfil actualizado exitosamente', 'success')
except:
db.session.rollback()
flash('Error al actualizar perfil', 'danger')
return redirect(url_for('perfil'))
@app.route('/cambiar-password', methods=['POST'])
@login_required
def cambiar_password():
password_actual = request.form.get('password_actual')
password_nuevo = request.form.get('password_nuevo')
password_confirmar = request.form.get('password_confirmar')
if not current_user.check_password(password_actual):
flash('Contraseña actual incorrecta', 'danger')
return redirect(url_for('perfil'))
if password_nuevo != password_confirmar:
flash('Las contraseñas no coinciden', 'danger')
return redirect(url_for('perfil'))
if len(password_nuevo) < 6:
flash('La contraseña debe tener al menos 6 caracteres', 'danger')
return redirect(url_for('perfil'))
current_user.set_password(password_nuevo)
db.session.commit()
flash('Contraseña cambiada exitosamente', 'success')
return redirect(url_for('perfil'))
if __name__ == '__main__':
app.run(debug=True)Templates Base (templates/base.html)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Sistema de Login{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">🔐 Sistema Login</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('perfil') }}">Mi Perfil</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Cerrar Sesión</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Iniciar Sesión</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('registro') }}">Registrarse</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>Login (templates/login.html)
{% extends 'base.html' %}
{% block title %}Iniciar Sesión{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body">
<h2 class="text-center mb-4">Iniciar Sesión</h2>
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Usuario</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Contraseña</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Recordarme</label>
</div>
<button type="submit" class="btn btn-primary w-100">Iniciar Sesión</button>
</form>
<hr>
<p class="text-center">¿No tienes cuenta? <a href="{{ url_for('registro') }}">Regístrate aquí</a></p>
</div>
</div>
</div>
</div>
{% endblock %}Registro (templates/registro.html)
{% extends 'base.html' %}
{% block title %}Registro{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body">
<h2 class="text-center mb-4">Crear Cuenta</h2>
<form method="POST" action="{{ url_for('registro') }}">
<div class="mb-3">
<label for="username" class="form-label">Usuario *</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email *</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="nombre" class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="nombre" name="nombre">
</div>
<div class="mb-3">
<label for="password" class="form-label">Contraseña *</label>
<input type="password" class="form-control" id="password" name="password" minlength="6" required>
<small class="form-text text-muted">Mínimo 6 caracteres</small>
</div>
<div class="mb-3">
<label for="password2" class="form-label">Confirmar Contraseña *</label>
<input type="password" class="form-control" id="password2" name="password2" required>
</div>
<button type="submit" class="btn btn-success w-100">Registrarse</button>
</form>
<hr>
<p class="text-center">¿Ya tienes cuenta? <a href="{{ url_for('login') }}">Inicia sesión</a></p>
</div>
</div>
</div>
</div>
{% endblock %}Dashboard (templates/dashboard.html)
{% extends 'base.html' %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Dashboard</h1>
<p class="lead">Bienvenido, {{ current_user.username }}!</p>
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">📊 Estadísticas</h5>
<p class="card-text">Ver tus estadísticas y métricas</p>
<a href="#" class="btn btn-primary">Ver Más</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">👤 Mi Perfil</h5>
<p class="card-text">Editar información de tu perfil</p>
<a href="{{ url_for('perfil') }}" class="btn btn-primary">Ver Perfil</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">⚙️ Configuración</h5>
<p class="card-text">Configurar preferencias</p>
<a href="#" class="btn btn-primary">Configurar</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}Perfil (templates/perfil.html)
{% extends 'base.html' %}
{% block title %}Mi Perfil{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8 mx-auto">
<h1>Mi Perfil</h1>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Información del Usuario</h5>
<p><strong>Usuario:</strong> {{ usuario.username }}</p>
<p><strong>Email:</strong> {{ usuario.email }}</p>
<p><strong>Nombre:</strong> {{ usuario.nombre or 'No especificado' }}</p>
<p><strong>Miembro desde:</strong> {{ usuario.fecha_registro.strftime('%d/%m/%Y') }}</p>
<p><strong>Último acceso:</strong> {{ usuario.ultimo_acceso.strftime('%d/%m/%Y %H:%M') if usuario.ultimo_acceso else 'N/A' }}</p>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Editar Perfil</h5>
<form method="POST" action="{{ url_for('editar_perfil') }}">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre</label>
<input type="text" class="form-control" id="nombre" name="nombre" value="{{ usuario.nombre or '' }}">
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ usuario.email }}">
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Cambiar Contraseña</h5>
<form method="POST" action="{{ url_for('cambiar_password') }}">
<div class="mb-3">
<label for="password_actual" class="form-label">Contraseña Actual</label>
<input type="password" class="form-control" id="password_actual" name="password_actual" required>
</div>
<div class="mb-3">
<label for="password_nuevo" class="form-label">Nueva Contraseña</label>
<input type="password" class="form-control" id="password_nuevo" name="password_nuevo" minlength="6" required>
</div>
<div class="mb-3">
<label for="password_confirmar" class="form-label">Confirmar Nueva Contraseña</label>
<input type="password" class="form-control" id="password_confirmar" name="password_confirmar" required>
</div>
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}Mejoras Adicionales
Recuperación de Contraseña
from flask_mail import Mail, Message
import secrets
mail = Mail(app)
@app.route('/recuperar-password', methods=['GET', 'POST'])
def recuperar_password():
if request.method == 'POST':
email = request.form.get('email')
usuario = Usuario.query.filter_by(email=email).first()
if usuario:
# Generar token
token = secrets.token_urlsafe(32)
# Guardar token en BD (necesitas agregar campo)
# Enviar email
msg = Message('Recuperar Contraseña',
sender='noreply@ejemplo.com',
recipients=[email])
msg.body = f"Token de recuperación: {token}"
mail.send(msg)
flash('Email de recuperación enviado', 'info')
else:
flash('Email no encontrado', 'danger')
return render_template('recuperar.html')Roles y Permisos
class Rol(db.Model):
id = db.Column(db.Integer, primary_key=True)
nombre = db.Column(db.String(50), unique=True)
usuarios = db.relationship('Usuario', backref='rol')
# Agregar a Usuario
rol_id = db.Column(db.Integer, db.ForeignKey('rol.id'))
# Decorador para verificar rol
from functools import wraps
def rol_requerido(rol_nombre):
def decorador(f):
@wraps(f)
def funcion_decorada(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('login'))
if current_user.rol.nombre != rol_nombre:
flash('No tienes permisos', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return funcion_decorada
return decorador
@app.route('/admin')
@rol_requerido('admin')
def admin_panel():
return render_template('admin.html')Seguridad Adicional
- HTTPS: Usa siempre en producción
- CSRF Protection: Flask-WTF incluye protección
- Rate Limiting: Limita intentos de login
- 2FA: Implementa autenticación de dos factores
- Sesiones seguras: Configura cookies seguras
- Logs: Registra intentos fallidos
Conclusión
Has creado un sistema completo de autenticación con Flask. Este sistema incluye todas las funcionalidades básicas: registro, login, sesiones, protección de rutas y gestión de usuarios. Puedes extenderlo agregando más funcionalidades como roles, permisos, recuperación de contraseña y autenticación de dos factores.