El ciclo completo de una sesión
Iniciar, refrescar, caducar, cerrar y revocar desde otro dispositivo. Los cinco momentos de una sesión persistente y qué hace tu aplicación en cada uno.
Qué se guarda
La primera decisión, y la que más se equivoca: en el almacén va el testigo de refresco, no el de acceso.
Si tu servicio no distingue los dos, y te da un solo testigo de larga duración, funciona igual — pero merece saber que estás en el peor de los dos mundos: vida larga y sin la separación que permite acortarla.
Los cinco momentos
Los tres errores
Uno: no guardar el refresco rotado. Muchos servicios devuelven un refresco nuevo con cada uso e invalidan el anterior. Si tu aplicación lo usa y no guarda el nuevo, la próxima vez presentará uno ya invalidado y el usuario tendrá que iniciar sesión otra vez. Síntoma: «a veces me echa y no sé por qué», con un patrón que depende de cuándo se cerró la aplicación.
Dos: refrescar dos veces a la vez. Si dos operaciones detectan a la vez que el testigo de acceso caducó, las dos refrescan; con rotación, la segunda usa un refresco que la primera acaba de invalidar. Es una condición de carrera clásica y se resuelve con lo de la fase IV:
// un solo refresco en vuelo: los demás esperan al mismo resultadostruct Sesion { testigo: RwLock<Option<Testigo>>, refrescando: Mutex<()>, // serializa el refresco}async fn asegurar_testigo(s: &Sesion) -> Result<String, Error> { if let Some(t) = s.vigente().await { return Ok(t); } let _guardia = s.refrescando.lock().await; // volver a comprobar: otro pudo refrescar mientras esperábamos el cerrojo if let Some(t) = s.vigente().await { return Ok(t); } s.refrescar().await}La segunda comprobación después de tomar el cerrojo es la parte que se olvida y es lo que evita que todos los que esperaban refresquen en fila.
Tres: cerrar sesión solo por un lado. Borrar el testigo local sin avisar al servicio deja una sesión válida en el servidor — que es lo que un usuario intenta evitar al cerrar sesión en un equipo prestado. Y al revés: revocar sin borrar deja un testigo muerto que producirá un fallo confuso en el próximo arranque.
#[tauri::command]async fn cerrar_sesion(estado: State<'_, Sesion>) -> Result<(), Error> { // avisar al servicio, y no dejar que su fallo impida el borrado local if let Some(t) = estado.testigo.read().await.as_ref() { let _ = api::revocar(t).await; } secretos::borrar(&estado.cuenta().await)?; // esto sí tiene que pasar *estado.testigo.write().await = None; Ok(())}El orden importa: primero el intento de revocar, después el borrado local que ocurre pase lo que pase. Si el servicio no responde —el usuario está sin conexión— el cierre local sigue siendo correcto y el testigo caducará solo.
Y la sesión que no debería recordarse
Un detalle de producto que encaja aquí: la casilla de «mantener la sesión iniciada». Si tu aplicación la ofrece —y en un equipo compartido debería—, la diferencia es exactamente si el refresco va al almacén o solo al estado del core.
Es una línea de código y cambia el comportamiento en el caso que más importa: un ordenador prestado, o de trabajo, o de una biblioteca.