Retornar datos de sesión
Cuando un usuario o empleado inicia sesión en la plataforma y crea algún tipo de registro, por ejemplo una solicitud de recursos humanos, la aplicación que realiza la inserción necesita saber qué usuario autenticado está realizando la operación.
En esta práctica, la autenticación la gestiona Flask, pero la inserción puede realizarla una aplicación Java/Tomcat o PHP. Para resolverlo, Flask devolverá los datos básicos de sesión al reverse proxy, y Nginx Proxy Manager los reenviará a la aplicación mediante cabeceras HTTP.
El flujo general será:
Usuario inicia sesión en Flask
↓
Flask guarda en sesión user_id, user y roles
↓
El usuario accede a una ruta protegida
↓
Nginx pregunta a Flask si puede acceder
↓
Flask responde con X-User-Id, X-Username y X-Role
↓
Nginx reenvía esas cabeceras a Java/PHP
↓
Java/PHP usa el id del usuario en sus operaciones
La responsabilidad queda dividida de esta forma:
Flask
- Autentica al usuario.
- Guarda la sesión.
- Guarda user_id, user y roles.
- Responde a /auth/verify.
Nginx Proxy Manager
- Protege rutas.
- Pregunta a Flask si el usuario tiene permiso.
- Recoge X-User-Id, X-Username y X-Role.
- Reenvía esas cabeceras a Java o PHP.
Java/PHP
- No gestionan el login.
- Leen X-User-Id, X-Username y X-Role.
- Usan el id del usuario para guardar quién realiza cada operación.
Video de ayuda
1. Cambiar la función SQL
La función login_empleado inicialmente devuelve un valor de tipo BOOLEAN:
truesi el login es correcto.falsesi el login es incorrecto.
Para poder conocer el identificador del empleado autenticado, se modifica la función para que devuelva un INTEGER con el id del empleado.
Si el login es incorrecto, devolverá NULL.
Si ya existe la función anterior con el mismo nombre y parámetros, puede ser necesario eliminarla primero, ya que PostgreSQL no siempre permite cambiar directamente el tipo de retorno con CREATE OR REPLACE.
DROP FUNCTION IF EXISTS login_empleado(VARCHAR, VARCHAR);
Nueva función:
CREATE OR REPLACE FUNCTION login_empleado(
_login VARCHAR,
_passwd VARCHAR
)
RETURNS INTEGER
AS $$
DECLARE
v_id INTEGER;
v_passwd_almacenada VARCHAR(255);
BEGIN
SELECT id, passwd
INTO v_id, v_passwd_almacenada
FROM empleados
WHERE correo = _login OR nombre = _login
LIMIT 1;
IF NOT FOUND THEN
RETURN NULL;
END IF;
IF crypt(_passwd, v_passwd_almacenada) = v_passwd_almacenada THEN
RETURN v_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Con este cambio:
Login correcto → devuelve el id del empleado
Login incorrecto → devuelve NULL
2. Modificar la función login_empleado en Flask
En Flask, la función Python puede mantenerse, pero ahora su resultado será el id del empleado o None.
def login_empleado(user, passwd):
"""Devuelve el id del empleado si el login es correcto; si no, devuelve None."""
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("SELECT login_empleado(%s, %s);", (user, passwd))
return cur.fetchone()[0]
3. Guardar el id en la sesión de Flask
En el login de Flask, cuando el usuario se autentica correctamente, se almacena el identificador del empleado en la sesión.
empleado_id = login_empleado(user, passwd)
if empleado_id is not None:
session.permanent = True
session["login"] = True
session["user"] = user
session["user_id"] = empleado_id
session["roles"] = ["empleado"]
if next_url_segura(next_url):
return redirect(next_url)
return redirect(url_for("index"))
Para el usuario administrador definido mediante variables de entorno:
if user == USUARIO and passwd == PASSWD:
session.permanent = True
session["login"] = True
session["user"] = user
session["user_id"] = 0
session["roles"] = ["admin"]
if next_url_segura(next_url):
return redirect(next_url)
return redirect(url_for("index"))
4. Devolver datos de sesión desde /auth/verify
Para que Nginx Proxy Manager pueda pasar los datos del usuario autenticado a Java o PHP, el endpoint /auth/verify debe devolver cabeceras HTTP.
Primero se añade make_response al import de Flask:
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, make_response
Después se modifica el endpoint /auth/verify:
@app.route("/auth/verify")
def auth_verify():
"""
Endpoint para validación desde reverse proxy.
Respuestas:
- 204: permitido.
- 401: no autenticado.
- 403: autenticado, pero sin rol requerido.
"""
if not session.get("login"):
return "", 401
roles_usuario = session.get("roles", [])
if isinstance(roles_usuario, str):
roles_usuario = [roles_usuario]
def respuesta_permitida():
response = make_response("", 204)
response.headers["X-User-Id"] = str(session.get("user_id", ""))
response.headers["X-Username"] = session.get("user", "")
response.headers["X-Role"] = ",".join(roles_usuario)
return response
roles_requeridos = request.headers.get("X-Required-Roles", "").strip()
if not roles_requeridos:
return respuesta_permitida()
lista_roles_requeridos = [
rol.strip()
for rol in roles_requeridos.split(",")
if rol.strip()
]
for rol in lista_roles_requeridos:
if rol in roles_usuario:
return respuesta_permitida()
return "", 403
De esta forma, cuando un usuario autenticado tenga permiso, Flask devolverá cabeceras como estas:
X-User-Id: 3
X-Username: gaspar
X-Role: empleado
5. Configuración general del reverse proxy
Esta configuración se coloca en la sección avanzada/general del Proxy Host de Nginx Proxy Manager.
- Cambia
practica4-flaskapp-1por el nombre real del contenedor o servicio de Flask en tu stack. - Puedes comprobar los nombres disponibles con:
docker network inspect NOMBRE_RED
- El rol indicado en
X-Required-Rolesdebe coincidir exactamente con el rol guardado en Flask. - Por ejemplo, si Flask guarda
session["roles"] = ["empleado"], Nginx debe pedirempleado, nousuarionicliente.
location = /_flask_auth_admin {
internal;
proxy_pass http://practica4-flaskapp-1:8888/auth/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Required-Roles "admin";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Host $host;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /_flask_auth_empleado {
internal;
proxy_pass http://practica4-flaskapp-1:8888/auth/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Required-Roles "empleado";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Host $host;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location @login_redirect {
return 302 /login?next=$request_uri;
}
location @forbidden {
return 403;
}
6. Configurar una ruta protegida para empleado
En cada ruta que deba ser accesible por empleados, se añade:
auth_request /_flask_auth_empleado;
auth_request_set $auth_user_id $upstream_http_x_user_id;
auth_request_set $auth_username $upstream_http_x_username;
auth_request_set $auth_role $upstream_http_x_role;
error_page 401 = @login_redirect;
error_page 403 = @forbidden;
proxy_set_header X-User-Id $auth_user_id;
proxy_set_header X-Username $auth_username;
proxy_set_header X-Role $auth_role;
Por ejemplo, para una página Java de empleado:
/vistas/protegida_usuario.jsp
7. Configurar una ruta protegida para administrador
En cada ruta que deba ser accesible solo por administradores, se añade:
auth_request /_flask_auth_admin;
auth_request_set $auth_user_id $upstream_http_x_user_id;
auth_request_set $auth_username $upstream_http_x_username;
auth_request_set $auth_role $upstream_http_x_role;
error_page 401 = @login_redirect;
error_page 403 = @forbidden;
proxy_set_header X-User-Id $auth_user_id;
proxy_set_header X-Username $auth_username;
proxy_set_header X-Role $auth_role;
Por ejemplo:
/vistas/protegida_admin.jsp
8. Obtener datos de sesión en Java
En Java, los datos se obtienen desde las cabeceras HTTP que ha añadido Nginx después de validar la sesión en Flask.
En un controlador o servlet:
String userIdHeader = request.getHeader("X-User-Id");
String username = request.getHeader("X-Username");
String roleHeader = request.getHeader("X-Role");
if (userIdHeader == null || userIdHeader.isBlank()) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Usuario no autenticado");
return;
}
int idUsuario = Integer.parseInt(userIdHeader);
9. Uso correcto con DAO
No es recomendable leer las cabeceras HTTP directamente dentro del DAO.
El DAO no debería conocer request, porque request pertenece a la capa web.
La forma correcta es:
Controlador lee X-User-Id
↓
Controlador convierte el id a int
↓
Controlador llama al DAO pasando idUsuario
↓
DAO usa idUsuario en el INSERT
Ejemplo de controlador:
String userIdHeader = request.getHeader("X-User-Id");
if (userIdHeader == null || userIdHeader.isBlank()) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Usuario no autenticado");
return;
}
int idUsuario = Integer.parseInt(userIdHeader);
String titulo = request.getParameter("titulo");
String descripcion = request.getParameter("descripcion");
SolicitudDAO dao = new SolicitudDAO();
dao.insertarSolicitud(titulo, descripcion, idUsuario);
Ejemplo de DAO:
public void insertarSolicitud(String titulo, String descripcion, int idUsuario) throws SQLException {
String sql = """
INSERT INTO solicitudes (titulo, descripcion, empleado_id)
VALUES (?, ?, ?)
""";
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, titulo);
ps.setString(2, descripcion);
ps.setInt(3, idUsuario);
ps.executeUpdate();
}
}