Proyecto: Sistema de Login con Flask y SQLite

👤 Admin 📅 26 de octubre, 2025 ⏱ 21 min 🏷 Proyectos Python

¿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ñas

Estructura 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.css

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