8.5 KiB
Galerías Fotográficas - Análisis técnico
Relacionado
Arquitectura del sistema
Queremos hacer un sistema modular, con componentes independientes que se puedan desarrollar, probar e implementar de forma aislada. Esto permitirá una mayor flexibilidad y escalabilidad en el desarrollo del sistema.
Queremos abstraer todo lo posible las dependencias entre capas. De tal forma que el frontal y backend puedan intercambiarse con diferentes implementaciones sin afectar al resto del sistema. Para ello, vamos a hacer el backend database-agnostic, puediendo conectar con diferentes servidores de bases de datos. La responsabilidad de escoger motor de base de datos recaerá sobre el cliente. Nosotros usaremos Sqlite para desarrollo local y PostgreSQL para producción.
Para poder desarrollarlo rápidamente y tener una base sobre la que iterar, empezaremos con un monolito modular. Este monolito quedará estructurado en módulos que puedan migrarse a microservicios en el futuro. Comandaremos el desarrollo mediante DDD (Domain-Driven Design) y CQRS (Command Query Responsibility Segregation). Algunos procesos como el procesado de imágenes se harán de forma desacoplada y asíncrona.
Para el frontal, Angular nos ofrece componentes que usaremos para componer vistas siguiendo una arquitectura MVVM (Model-View-ViewModel).
Para que el frontend y el backend no dependan entre ellos, vamos a establecer una serie de estándares de comunicación. Siempre usaremos el estandar HTTP mediante TLS. (HTTPS) En toda comunicación se preferirá enviar y recibir los datos mediante el body de la petición.
Toda la información sensible, como contraseñas, se enviará cifrada mediante RSA 256.
Para ello, el cliente generará un thumprint único que incluirá el user-agent y un dato único del navegador mas un UUIDv4; hará una petición HEAD al endpoint /security/rsa
enviando el thumbprint en el header XXX-Thumbprint
.
El cliente recibirá una respuesta vacía (204 No Content) con un header XXX-encryption-key
que se utilizará para cifrar los valores de los campos concretos.
En la siguiente petición, donde se enviarán los datos sensibles, el cliente incluirá XXX-Thumbprint
en el header.
Al recibir la petición, tendrá que rotar el thumbprint.
Cada thumbprint será válido durante 5 minutos en caso de conexiones lentas. Sin embargo, no se permitirá su reutilización.
En caso de que hubiese un MIM (Man-In-The-Middle) o cualquier otro tipo de atacante tratando de interceptar la comunicación, debe ser capaz de detectar y leer el doble cifrado de HTTPS + thumbprint-rsa.
De esta forma, todas las peticiones deben cumplir con el siguiente contrato:
- Como requisito indispensable, toda la comunicación debe realizarse mediante HTTPS.
- Todas las peticiones deben incluir un header
XXX-Thumbprint
con el thumbprint actual. - Todas las peticiones incluirán un body que sigue el patron
Request<T>
. - Las peticiones con datos sensibles, tendrán sus datos cifrados mediante RSA 256.
El patrón Request<T>
incluirá un campo con los datos de la petición, otro con el thumprint duplicado y un último campo que indica si tiene datos sensibles o no.
Con una implementación como la que sigue:
public class Request<T> where T : IDTOSerializable
{
public T Data { get; set; }
public string Thumbprint { get; set; }
public bool HasSensitiveData { get; set; }
}
En el caso de errores, seguiremos el estandar Problem Details extendido.
En el caso de respuestas exitosas, las apis responderan mediante el patron Response<T>
.
Una Response<T>
estará compuesta por diferentes campos que indicarán el estado de la transacción, el resultado de la transacción, y los posibles errores lógicos pertenecientes a los datos enviados.
De esta forma, garantizamos un contrato como el siguiente:
public class Response<T> where T : IDTOSerializable
{
public bool IsSuccess { get; set; } = false;
public T Result { get; set; }
public List<DataError>? Errors { get; set; } = null;
public static Response<T> Success(T result)
{
return new Response<T>
{
IsSuccess = true,
Result = result
};
}
public static Response<T> Failure(T result, List<DataError> errors)
{
return new Response<T>
{
IsSuccess = false,
Result = result,
Errors = errors
};
}
}
En caso de que los datos enviados al backend provoquen un error lógico, por ejemplo: una imagen que se ha subido no se puede guardar por que ha llegado corrupta; devolveremos una Response<T>
que contenga un DataError
como este:
public class DataError
{
public string Message { get; set; }
public string? Details { get; set; }
}
De esta forma, el cliente podrá interpretar los errores y mostrarlos al usuario de forma adecuada.
La autenticación y autorización se manejarán mediante JWT (JSON Web Tokens) y OAuth 2.0.
Para ello el front atacará al backend para obtener el token de acceso y luego lo incluirá en las cabeceras de las peticiones con este formato Authorization: Bearer <token>
.
También incluirá el token de refresco en las peticiones que requieran autenticación, con este formato XXX-Refresh-Token: <token>
.
El stack
Backend
Vamos a usar ASP.NET Core como framework principal, junto con Entity Framework Core para la gestión de la base de datos. Redis se utilizará como caché distribuido para mejorar el rendimiento. PostgreSQL se utilizará como sistema de gestión de bases de datos relacional. En desarrollo se utilizará SQLite.
Frontend
Vamos a usar Angular como framework principal, junto con TailwindCSS para el diseño y NgRx para la gestión del estado. Vite como empaquetador y Node.js como entorno de ejecución. Usaremos RxJS para la programación reactiva.
Base de datos: Esquema y relaciones
Infraestructura
Inicialmente se utilizará un enfoque monolítico, pero se diseñará con la posibilidad de escalar a microservicios en el futuro. El producto final debe poder desplegarse en docker y kubernetes. Para ello usarmos Podman como herramienta para gestionar los contenedores. Usaremos una caché para resultados intermedios, como el portfolio.
Seguridad
Para mantener seguro el sistema obligaremos el uso de HTTPS en todas las comunicaciones. Los usuarios deberán autenticarse mediante OAuth 2.0 y OpenID Connect usando JWT (JSON Web Tokens) para la gestión de sesiones. Firmaremos los tokens JWT con una clave secreta almacenada de forma segura. Validaremos la firma siempre. El certificado usado variará cada 24 horas y en cada reinicio. Los JWT tendrán asociados un Refresh Token que permitirá obtener nuevos tokens de acceso sin necesidad de volver a autenticarse. Para protegernos contra ataques CSRF (Cross-Site Request Forgery), implementaremos tokens CSRF en todas las solicitudes que modifiquen datos. Habilitaremos CORS (Cross-Origin Resource Sharing) para permitir que solo dominios específicos puedan acceder a nuestra API. Implementaremos políticas de contraseñas seguras, incluyendo longitud mínima, complejidad y expiración periódica. Además, se instará a los usuarios a habilitar la autenticación de dos factores (2FA) para añadir una capa adicional de seguridad. Todos los usuarios que inicien sesión mediante usuario y contraseña recibirán un correo electrónico de acceso único, con un token de acceso que caducará en 5 minutos. Para proteger los datos sensibles, como contraseñas y tokens, se utilizará hashing y cifrado. Implementaremos un sistema de roles y permisos para controlar el acceso a diferentes partes del sistema.
Para evitar ataques de fuerza bruta y ataques de tiempo, implementaremos 4 políticas:
- Limitar el número de intentos de inicio de sesión fallidos y bloquear el acceso temporalmente.
- Implementar un sistema de CAPTCHA que aparecerá aleatoriamente en cada intento de inicio de sesión y después de 3 intentos fallidos.
- Monitorear y registrar los intentos de inicio de sesión para detectar patrones sospechosos.
- Retrasar aleatoriamente las respuestas del servidor una cantidad de tiempo variable inferior a los 3 segundos. (Sacrificamos un poco de la experiencia por algo más de seguridad)
Guardaremos el mínimo número de cookies posibles y evitaremos usar cookies de sesión. Todas las cookies que usemos serán seguras, HttpOnly y SameSite=Strict.