Seguridad y fiabilidad
ago 9, 2025 21:20 • 36 visitas
Dejando de lado las analíticas proporcionadas por herramientas de terceros como Google Analytics, el recuento propio de visitas de una aplicación web antiguamente se hacía capturando la IP del usuario. Desde hace un par de años, la mayor parte de navegadores incluyen, por defecto, una función para ocultar la dirección, motivo por el cual ya no es recomendable proceder de esa manera.
La primera solución que se le ocurriría a cualquier desarrollador -también a mí- sería la de guardar un conjunto cookies que almacenen la fecha y hora en que se visualiza el contenido, para que, en una visualización posterior, el servidor pueda comprobar el tiempo que ha transcurrido y decidir si se trata de una visita nueva o no. Pero, usted imagine al autor que ha publicado en su app alrededor de 150 entradas, que tampoco son demasiadas. Si ha de enviar una cookie al usuario por cada entrada que publica, ¿no le parece un abuso guardar 150 cookies en su navegador? ¿Qué hacemos entonces?
Una función que haga uso de localStorage podría sacarnos del apuro. En contraposición a las cookies -que no pueden superar los 4 KB- localStorage nos permite guardar hasta 5 MB por origen, sin límite de claves, lo que da mucho juego. Preste atención a la siguiente función de React:
export const visit = async (id: number) => {
const visits = JSON.parse(localStorage.getItem("visit") || "{}");
const lastVisit = visits[id];
const now = new Date();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (!lastVisit || new Date(lastVisit) < oneYearAgo) {
visits[id] = now.toISOString();
localStorage.setItem("visit", JSON.stringify(visits));
try {
await axios.post("https://your-app.com/api/register-visit/", {id: String(id)});
} catch (e) {
console.error("error registering visit: ", e);
}
}
}
La función realiza una petición POST a la API, con la que la API puede registrar su visita, si es la primera visualización del contenido o si ha transcurrido más de un año desde la última visita. Parece sencillo, pero tiene un problema: es fácil de jaquear. Basta con escribir en la consola el siguiente script, que borra -a intervalos de un segundo- cualquier rastro que usted pueda haber dejado, forzando a la API a contabilizar visitas que no son tales:
(() => {
setInterval(() => {
localStorage.clear();
}, 1000);
})();
Entonces, si no podemos contabilizar las visitas con el seguimiento de la IP, si no podemos emplear cookies porque sobrecargan al usuario y si no podemos usar localStorage porque no es seguro, ¿qué podemos hacer? Bueno, pues ahí va mi solución: si el usuario puede borrar localStorage en cualquier momento, ¿por qué no hacemos un backup de localStorage en sessionStorage para que la información persista durante toda la sesión?
Mientras el usuario no borre localStorage, este va a persistir incluso si se apaga el ordenador. Por su parte, sessionStorage es imposible de borrar mientras dura la sesión, es decir, mientras la pestaña de la aplicación sigue abierta. La idea de hacer un backup en sessionStorage responde a la necesidad de evitar que alguien haga desaparecer la información almacenada en localStorage con la sesión abierta. La solución quedaría implementada entonces de la siguiente manera:
export const visit = async (id: number) => {
const visits = JSON.parse(localStorage.getItem("visits") || "{}");
const lastVisit = visits[id];
const now = new Date();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (!lastVisit || new Date(lastVisit) < oneYearAgo) {
visits[id] = now.toISOString();
sessionStorage.setItem("visits", JSON.stringify(visits));
localStorage.setItem("visits", JSON.stringify(visits));
try {
await axios.post("https://your-app.com/api/register-visit/", {id: String(id)});
} catch (e) {
console.error("error registering visit: ", e);
}
}
}
export const gVisits = async (posts: { id: number }[]) => {
const localData = JSON.parse(localStorage.getItem("visits") || "{}");
const sessionData = JSON.parse(sessionStorage.getItem("visits") || "{}");
const recoveredVisits = { ...sessionData, ...localData };
if (!localStorage.getItem("visits")) {
localStorage.setItem("visits", JSON.stringify(recoveredVisits));
}
for (const post of posts) {
if (!recoveredVisits[post.id]) {
recoveredVisits[post.id] = new Date().toISOString();
}
}
sessionStorage.setItem("visits", JSON.stringify(recoveredVisits));
localStorage.setItem("visits", JSON.stringify(recoveredVisits));
}
Mientras, en el componente podríamos un useEffect que iría registrando y guardando copias de las visitas de las entradas que ese mismo componente renderiza a intervalos de 500 ms. Y el intervalo no ha sido elegido al azar, pues coincide con el límite de carrera que obligaría a la infraestructura del backend a activar la protección por ataque DDoS.
useEffect(() => {
if (posts.length > 0) {
posts.forEach((e) => visit(e.id).then(() => {
console.log("registered visit.")
}));
}
const interval = setInterval(() => gVisits(posts.map(post => ({ id: post.id }))), 500);
return () => clearInterval(interval);
}, [posts]);
Esta no es una solución perfecta, pero es mejor que todas las anteriores. Y me temo que hoy en día es la mejor a nuestro alcance. Si eres desarrollador y tienes alguna otra idea mejor que esta, ponte en contacto conmigo y házmelo saber. Te estaré eternamente agradecido.
Antes de continuar...
Detrás de cada artículo de Bitácora hay tiempo, investigación y desarrollo continuo. Si quiere ayudar a mantener este proyecto libre e independiente, apóyeme: