Vida de una sesión

En el archivo de configuración php.ini de mi servidor local podemos ver esta entrada:

; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime = 1440
Recuerde que todo lo que aparece aquí ha sido probado en mi servidor local Apache+PHP montado para el aprendizaje, pues no conviene exponer las configuración del servidor real donde está viendo estas páginas por una cuestión de seguridad.

La máxima duración de una sesión se establece por defecto en 1440 segundos (24 minutos). La traducción del comentario dice que "Después de este número de segundos, los datos almacenados serán vistos como 'basura' y limpiados por el proceso de recolección de basura." La recolección de basura sucede durante el inicio de sesiones pero no siempre, pues depende de las siguientes configuraciones del php.ini:

; Defines the probability that the 'garbage collection' process is started
; on every session initialization. The probability is calculated by using
; gc_probability/gc_divisor. Where session.gc_probability is the numerator
; and gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; the gc will run on any give request.
; Default Value: 1
; Development Value: 1
; Production Value: 1
; http://php.net/session.gc-probability
session.gc_probability = 1

; Defines the probability that the 'garbage collection' process is started on every
; session initialization. The probability is calculated by using the following equation:
; gc_probability/gc_divisor. Where session.gc_probability is the numerator and
; session.gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; the gc will run on any give request. Increasing this value to 1000 will give you
; a 0.1% chance the gc will run on any give request. For high volume production servers,
; this is a more efficient approach.
; Default Value: 100
; Development Value: 1000
; Production Value: 1000
; http://php.net/session.gc-divisor
session.gc_divisor = 100    
    
Algunas configuraciones pueden modificarse en tiempo de ejecución con la función ini_set(a, b), donde "a" será el string de la variable de configuración y "b" será el nuevo valor en segundos a poner. Estas tres variables session.gc_maxlifetime, session.gc_probability y session.gc_divisor pueden modificarse en tiempo de ejecución.

En definitiva, una sesión tiene una duración de 24 minutos. Pasado este tiempo si no se ha usado, entonces pasará a ser basura. Pero el recolector la limpiará de la carpeta Temp sólo cuando se inicien nuevas sesiones pero no en todos los inicios. La probabilidad de que se inicie el recolector será la división entre session.gc_probability y session.gc_divisor. Tal como está por defecto esta probabilidad es del 1%, aunque en sistemas en producción (en un servidor real) es conveniente reducirla poniendo el valor 1000 para session.gc_divisor.

Para probar que esto funciona, durante las pruebas con las sesiones del ejemplo anterior comprobé en la carpeta Temp de mi servidor local que habían muchas sesiones registradas. Pero también observé que sólo estaban las más recientes, pues las de días anteriores habían desaparecido. El recolector de basura había estado limpiándolas. Entonces modifique el session.gc_probability con un valor 100, así que la probabilidad de limpieza con la apertura de una sesión debería ser del 100%. También modifiqué el session.gc_maxlifetime a 60 segundos para comprobar la duración de vida. Luego restauré el Apache y abrí la pagina1.php del ejemplo. Y, efectivamente, eliminó todas las sesiones del Temp antes de crear la nueva sesión. Luego esperé 1 minuto y cerré el navegador y abrí pagina1.php otra vez para comprobar que elimina esa sesión anterior pues ya se había vencido su vida, lo cual hizo antes de grabar la nueva sesión.

Por supuesto, es de entender que una probabilidad del 100% significa que el servidor estará continuamente ejecutando esa acción con cada apertura de sesión. Si el servidor tiene muchos usuarios abriendo sesiones, el sistema se ve muy forzado. Por eso se recomienda una probabilidad de 1/1000 (0.1%) en un caso real, aunque por defecto tiene 1/100 (1%).

Acceso indebido con sesiones vencidas y no limpiadas

Si antes de vencer el período de vida establecido se abre una página que llama a una sesión existente en el Temp, lo que se hace con sesion_start(), entonces el contador de tiempo vuelve a iniciarse para esa sesión. Pero también he comprobado que si una sesión tiene el período vencido pero aún no ha sido limpiado por el recolector de basura y se activa de nuevo con sesion_start(), también en este caso el contador vuelve a iniciarse.

Si las sesiones con el identificador y las variables de sesión se guardan en el servidor, parecería que no tendríamos porque preocuparnos. Si hay sesiones que no se están usando, vencidas o no, en la carpeta Temp ¿qué podría pasar?. Uno de los problemas sobre el que he leído es que muchas veces los sitios web se alojan en servidores compartidos, donde pudiera ser que todas las sesiones de todos los sitios de una máquina se alojaran en una misma carpeta Temp. ¿Podría otra persona acceder a las sesiones de mi sitio?. No tengo una respuesta para esta pregunta, pero sería recomendable no dejar sesiones sin usar en ningún lado.

Usando mi servidor local (no este real donde está viendo estas páginas) he ido a la carpeta Temp y he buscado la última sesion que he usado en los ejemplos de pagina1.php y siguientes. Esta sesión estaba vencida después de pasar más de 24 minutos pero dado que no había abierto ninguna sesión nueva, el recolector de basura ni siquiera tenía alguna probabilidad de limpiarla. Comprobado que contenía el color de fondo color-fondo|s:5:"green";, he abierto mi telnet con php para lanzar una petición al servidor usando pagina2.php con una cookie igual al identificador de sesión:

GET /temas/php-sesion/ejemplos/proceso-sesion/pagina2.php HTTP/1.1
Host: localhost
cookie: PHPSESSID=l2i5h7s7rdeooe74ecfqm64u42
Connection: Close

Y esta es la repuesta desde el servidor, donde hemos obviado alguna líneas no interesantes para el tema:

HTTP/1.1 200 OK
Date: Thu, 16 Sep 2010 17:52:00 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 2955
Connection: close
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>Página 2</title>
    ...
<!-- Insertamos en el body el color de fondo -->
<body style="background-color:green;" >
    ...

Se observa que se pudo reabrir la sesión vencida e incluso mostrarse el fondo de color verde. ¿Cómo podemos evitar esto?, es decir ¿cómo podemos evitar que se reabra una sesión sin que se usen los cauces previstos para ello?. El "cauce" previsto en nuestro ejemplo era ir por la pagina1.php. Si hemos cerrado y abierto el navegador, indepedientemente de que hayan sesiones vencidas o no en Temp, si vamos a pagina1.php el servidor construirá una nueva sesión. Esta debería ser la forma correcta para poder controlar las sesiones. Entonces el asunto estaría en destruir las sesiones que ya no necesitemos y no esperar por el "camión de la basura".

Destruyendo una sesión

Para destruir una sesión parece que podemos usar la función session_destroy(). Según dice el manual PHP, esta función destruye toda la información asociada con la sesión actual. No destruye ninguna de las variables globales asociadas con la sesión, ni destruye la cookie de sesión. Las variables pueden aún volver a usarse si volvemos a llamar a session_start(). En otro caso, si se desean eliminar, podemos iniciar el array con $_SESSION = array();. En cuanto a eliminar la cookie del usuario, debe usarse setcookie().

Pongámonos en marcha para comprobar esto. Antes de probar este ejemplo, primero tendríamos que tener abierta una sesión, lo cual puede hacerse llamando al ejemplo anterior pagina1.php desde aquí mismo, en caso en que en el tema anterior no hubiese ya entrado en ese ejemplo, donde podemos ir a la pagina3.php con un vínculo que nos lleva a la página destruye-sesion.php. El código importante de este ejemplo es:

<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_start();
//Primero destruimos las variables de sesión, en nuestro ejemplo "color-fondo"
$_SESSION = array();
//A continuación envíamos una cookie con tiempo negativo, lo que supone que
//el navegador eliminará la cookie en su sistema.
//Esto lo podemos hacer sólo si el php.ini está configurado para usar
//cookies, aunque por defecto es que sí.
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}
//Por último destruimos la sesión
session_destroy();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    ...

El HTML de esta página no tiene ningún contenido creado con PHP, aunque si tiene el mismo JavaScript que las de los otros ejemplos. Por eso obviamos el código pues no tiene mayor interés.

Se puede comprobar que destruye también el archivo de sesión de la carpeta Temp e incluso la cookie del navegador. Si ahora volvemos a pagina2.php o pagina3.php veremos que aparece con fondo rojo, indicativo de que no existe sesión abierta. Bueno parece que no queda ningún rastro y que esto es lo que estaba buscando. Pero queda un tema pendiente, ¿que pasa con la personalización del color del fondo?. Por lo visto no podemos usar sesiones para guardar particularidades de usuarios y, al mismo tiempo, querer destruirlas para evitar que alguién ajeno acceda a ellas.

Podríamos sólo destruir el archivo en Temp y dejar las variables de sesión vivas en memoria, $_SESSION["color-fondo"] en nuestro caso. Eso lo hacemos con el ejemplo destruye-parcial-sesion.php, cuyo vínculo también puede ver en la pagina3.php del ejemplo. El código es igual que el anterior, pero quitamos $_SESSION = array() y agregamos lo siguiente:

...
$un_color_fondo = "white";
if (isset($_SESSION["color-fondo"])){
    $un_color_fondo = htmlspecialchars($_SESSION["color-fondo"], ENT_QUOTES);
}
?>
...
<!-- Insertamos en el body el color de fondo -->
<body style="background-color:<?php echo $un_color_fondo; ?>;" >
...

Esto evidencia que la variable de sesión $_SESSION["color-fondo"] sigue estando en memoria. Este color lo aplicamos al cuerpo de la página. Lo único es que las variables de sesión sólo nos servirían para esta página, pues si volvemos a las páginas 2 o 3 veremos que aparecerán con el fondo rojo debido a que no hay sesión previamente abierta.

Pero no tiene ningún sentido destruir una sesión cuando lo que estamos buscando es precisamente poder usar sesiones, aunque tenemos que lograr que sean sesiones esperadas. En otro caso si podemos destruirlas.

Destruyendo sesiones inesperadas

En los ejemplos anteriores veíamos que se ponía el fondo de color rojo cuando se llegaba a las pagina2.php o pagina3.php sin antes haber pasado por pagina1.php, es decir, sin que existiera previamente una sesión abierta. Podemos modificar las páginas para destruir la sesión y redirigir al usuario a la pagina1.php. En este ejemplo repetimos esas 3 páginas y las renombramos en otra carpeta como pagina1b.php, pagina2b.php y pagina3b.php. El procedimiento será el mismo: entrando por la primera página abrimos sesión. Desde esa página tenemos vínculos a las siguientes manteniendo la sesión, pero si entramos por las siguientes sin sesión abierta previa destruiremos la sesión y nos redigirá a la primera página.

El ejemplo está disponible en este enlace a pagina1b.php

Se comprueba que funciona si copiamos la URL de la pagina2b.php e intentamos acceder directamente a ella. Por ejemplo, cerrando el navegador y abriéndolo de nuevo, ponemos en la barra de direcciones del navegador la ruta http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php para intentar acceder a la segunda página. Veremos que se abrirá la primera página.

También puede probarlo si, arrancando desde el navegador cerrado en caso de haber estado antes en pagina1b.php, usa este enlace que lleva a pagina2b.php o al de pagina3b.php, enlaces que redigirán a la primera página si no existe sesión previa.

La pagina1b.php tiene casi el mismo código que pagina1.php del ejemplo anterior, aunque sólo se modifica en esto:

//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
//Además le damos un nombre particular a esta sesión.
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_name("sesionB");
session_start();
...
    ...
    <script>
        var arrayCookies = document.cookie.split(";");
        for (var i=0; i<arrayCookies.length; i++){
            ...
            if (nombre == "sesionB") {
                ...
            }
        }
    </script>  
    ...

Hacemos notar lo resaltado en amarillo session_name("sesionB"), sentencia que debe ir antes de session_start(). En el tema anterior decíamos que el nombre de la sesión era PHPSESSID si no se especificaba otra cosa. Pero podemos manejar más de una sesión para un mismo navegador. Si previamente se usó el ejemplo del conjunto de páginas pagina1.php y siguientes, en caso de que aún no se hubiese cerrado el navegador esa sesión PHPSESSID estaría "viva". Eso quiere decir que su variable de sesión $_SESSION["color-fondo"] seguirá existiendo, por lo que si llamamos a este nuevo conjunto de páginas pagina1b.php y siguientes, tomará esa variable de sesión si tiene el mismo nombre. Por lo tanto si deseamos mantener este segundo conjunto de páginas protegidas en un entorno de variables independientes (aunque se llamen igual), hemos de darle un nombre de sesión distinto, tal como hemos hecho con session_name("sesionB"). La cookie del navegador también hemos de buscarla por este nombre nuevo de sesión.

Las páginas 2 y 3 sólo son modificadas en el script PHP, pues lo demás es igual. Vemos la pagina2b.php

<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
//Además le damos un nombre particular a esta sesión.
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_name("sesionB");
session_start();
//Establecemos una variable para el color del fondo por defecto
$un_color_fondo = "white";
//Comprobamos si la sesión ya fue iniciada
if (!isset($_SESSION["color-fondo"])){
    //Si se ha llegado aquí es que es una sesión inesperada.
    //Primero destruimos las variables de sesión
    $_SESSION = array();
    //A continuación eliminamos la cookie del navegador
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    //Por último destruimos la sesión
    session_destroy();
    //Rederigimos al usuario a pagina1b.php
    $host  = $_SERVER["HTTP_HOST"];
    $uri   = rtrim(dirname($_SERVER["PHP_SELF"]), "/\\");
    $extra = "pagina1b.php";
    header("Location: http://$host$uri/$extra");
    //Tras la redirección e inmediatamente salimos del script
    exit;
} else {
    //Aunque un parámetro de sesión proviene de una carpeta del
    //servidor, no está de más filtrar caracteres no deseados,
    //pues este color se insertará en el style del body.
    $un_color_fondo = htmlspecialchars($_SESSION["color-fondo-b"], ENT_QUOTES);
    //Si está página se está recibiendo por la ejecución del botón
    //submit del formulario, tomamos el color seleccionado
    if (isset($_POST["color"])){
        //Filtramos caracteres no deseados
        $un_color_fondo = htmlspecialchars($_POST["color"], ENT_QUOTES);
        //Eliminamos comillas por si el servidor tiene activado el
        //escape con barras invertidas
        if (get_magic_quotes_gpc() == 1) $un_color_fondo = stripslashes($un_color_fondo);
        //Por último actualizamos el parámetro de la sesión con el nuevo color
        $_SESSION["color-fondo-b"] = $un_color_fondo;
    }    
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
    ...

Para redirigir al usuario usamos la función header(). Nos fijamos en un ejemplo que hay en ese manual PHP, en relación con una nota que dice que HTTP/1.1 requiere una URI absoluta como argumento a Location. Expone un ejemplo que hemos trasladado. Por un lado la variable global $_SERVER nos permite acceder a las variables de entorno del servidor. Así $_SERVER["HTTP_HOST"] contiene el dominio de la página actual. Luego $_SERVER["PHP_SELF"] contiene la ruta del actual script que se está ejecutando. En el ejemplo la ruta completa del script es la que señalamos más arriba http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php por lo que $_SERVER["PHP_SELF"] contiene la ruta relativa al raíz, es decir, temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php.

Obtenemos la carpeta con dirname(). Por último con la función rtrim(ruta, "/\\") quitamos los espacios a la derecha de una ruta y también la última barra de la derecha. Ponemos las dos barras "/\" (aunque escapamos la segunda por estar dentro de un string), pues en Windows se pueden usar ambas y hemos de usar siempre las barras derecha, porque luego concatenemos todo con barras derecha. Inmediatamente después salimos del script con exit, con lo que no enviamos el HTML de la página 2 sino que redirigimos a la página 1.


Aunque hemos avanzado algo, esto no excluye la posibilidad de "robar" una sesión de la carpeta Temp y, mediante una aplicación como telnet con php, acceder a pagina2b.php sin que el servidor note la diferencia. En el próximo tema sobre sesiones expuestas intentaré buscar una solución.