Skip to content

Latest commit

 

History

History
222 lines (192 loc) · 11.2 KB

File metadata and controls

222 lines (192 loc) · 11.2 KB

6.2 ¿Cómo usar sesiones en Go?

En la sección 6.1, aprendimos que las sesiones son soluciones para la verificación de usuarios y que por ahora, la librería estándar de Go no tiene soporte para las sesiones o el manejo de ellas. Entonces, vamos a implementar nuestro propio manejador de sesiones en Go.

Creando sesiones

El principio básico detrás de las sesiones es que el servidor mantiene la información de cada cliente , y los clientes tienen una única sesión para acceder a su información. Cuando los usuarios visitan la aplicación web, el servidor crea una sesión siguiendo los siguientes pasos en tanto sean necesarios:

  • Crear un identificador de sesión único.
  • Abrir un espacio de almacenamiento: normalmente tenemos las sesiones en memoria, pero las perderás si el sistema accidentalmente se interrumpe. Esto puede ser un serio problema si la aplicación web maneja datos sensibles, como de comercio electrónico por ejemplo. Para solucionar este problema, puedes guardar los datos de sesión en la base de datos o en el sistema de ficheros. Eso hace que los datos sean mas fiables y fácil de compartir con otras aplicaciones, sin embargo la negociación de información es mas del lado del servidor para leer y escribir sesiones.
  • Enviar el identificador de sesión único al cliente.

El paso clave aquí es enviar un identificador único de sesión al cliente. En el contexto de una respuesta HTTP estándar, puedes usar el encabezado o el cuerpo que lo acompaña, por consiguiente, tenemos dos maneras de enviar identificadores de sesión a los clientes: por cookies o por URLs.

  • Cookies: el servidor puede fácilmente usar Set-cookie dentro del encabezado de la respuesta para enviar el identificador de sesión del cliente, y el cliente puede usar esta cookie para las futuras peticiones; usualmente definimos el tiempo de expiración de cookies que contienen información de sesión a 0, lo que significa que la cookie será guardada en memoria y será eliminada cuando el usuario cierre el navegador.
  • URLs: concatenar el identificador de sesión como argumento en todas las páginas. Esta opción puede sonar desordenada, pero es la mejor opción si el navegador tiene deshabilitadas las cookies.

Usando Go para manejar sesiones

Hemos hablado sobre la construcción de sesiones, y deberías tener una idea de como funciona, pero ¿cómo podemos usar sesiones en páginas dinámicas? Miremos de cerca al ciclo de vida de una sesión y luego podremos continuar la implementación de nuestro manejador de sesiones en Go.

Diseño del manejo de sesión

Aquí está una lista de algunas de las consideraciones claves para el diseño del manejo de sesión

  • Manejador de sesiones Global.
  • Mantener el identificador de sesión único.
  • Tener una sesión por cada usuario.
  • Almacenamiento de la sesión en memoria, archivos o base de datos.
  • Manejar las sesiones expiradas.

A continuación vamos a examinar un ejemplo completo de un manejador de sesiones en Go y el porqué de las decisiones de diseño que se tomaron.

Manejador de sesión

Definir un manejador de sesiones global:

	type Manager struct {
	    cookieName  string     //private cookiename
	    lock        sync.Mutex // protects session
	    provider    Provider
	    maxlifetime int64
	}

	func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
	    provider, ok := provides[provideName]
	    if !ok {
	        return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
	    }
	    return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
	}

Crear un manejador de sesiones global en la función main():

	var globalSessions *session.Manager
	// Then, initialize the session manager
	func init() {
	    globalSessions = NewManager("memory","gosessionid",3600)
	}

Como sabemos que podemos guardar las sesiones de muchas maneras, como en memoria, sistema de ficheros o directamente en la base de datos, necesitamos definir un interfaz Provider en orden de representar la estructura bajo nuestro manejador de sesiones:

	type Provider interface {
	    SessionInit(sid string) (Session, error)
	    SessionRead(sid string) (Session, error)
	    SessionDestroy(sid string) error
	    SessionGC(maxLifeTime int64)
	}
  • SessionInit implementa la inicialización de una sesión y retorna una nueva sesión si es exitoso.
  • SessionRead retorna una sesión representada con el identificador de sesión sid. Crea una nueva sesión y la retorna si no existe.
  • SessionDestroy dado un sid, elimina la sesión correspondiente.
  • SessionGC elimina las variables de sesión basado en el criterio maxLifeTime.

Entonces, ¿qué métodos debería tener nuestra interfaz de sesión? Si tienes alguna experiencia en desarrollo web deberías saber que solo hay cuatro operaciones para las sesiones: definir el valor, obtener el valor, eliminar el valor y obtener el identificador actual. Entonces en nuestra interfaz de sesión vamos a tener estos cuatro métodos para realizar estas operaciones.

	type Session interface {
	    Set(key, value interface{}) error //set session value
	    Get(key interface{}) interface{}  //get session value
	    Delete(key interface{}) error     //delete session value
	    SessionID() string                //back current sessionID
	}

Este diseño está basado en database/sql/driver, que define la interface primero, luego registra la estructura específica que queremos usar. El siguiente código es la implementación interna de nuestra función de registro.

	var provides = make(map[string]Provider)

	// Register makes a session provider available by the provided name.
	// If a Register is called twice with the same name or if the driver is nil,
	// it panics.
	func Register(name string, provider Provider) {
	    if provider == nil {
	        panic("session: Register provider is nil")
	    }
	    if _, dup := provides[name]; dup {
	        panic("session: Register called twice for provider " + name)
	    }
	    provides[name] = provider
	}

Identificacores de sesión únicos.

Los identificadores de sesión únics sirven para identificar usuarios en las aplicaciones web, por lo tanto deben ser únicos. El siguiente código muestra como lograr esto:

	func (manager *Manager) sessionId() string {
	    b := make([]byte, 32)
	    if _, err := io.ReadFull(rand.Reader, b); err != nil {
	        return ""
	    }
	    return base64.URLEncoding.EncodeToString(b)
	}

Crear una sesión

Necesitamos localizar u obtener una sesión existente en orden de validar las operaciones de usuario. La función SessionStart es para verificar la existencia de cualquier sesión relacionada al usuario actual, y crearla si no ninguna sesión es encontrada.

	func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
	    manager.lock.Lock()
	    defer manager.lock.Unlock()
	    cookie, err := r.Cookie(manager.cookieName)
	    if err != nil || cookie.Value == "" {
	        sid := manager.sessionId()
	        session, _ = manager.provider.SessionInit(sid)
	        cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
	        http.SetCookie(w, &cookie)
	    } else {
	        sid, _ := url.QueryUnescape(cookie.Value)
	        session, _ = manager.provider.SessionRead(sid)
	    }
	    return
	}

Aquí hay un ejemplo que usa la sesión de usuario para el ingreso.

	func login(w http.ResponseWriter, r *http.Request) {
	    sess := globalSessions.SessionStart(w, r)
	    r.ParseForm()
	    if r.Method == "GET" {
	        t, _ := template.ParseFiles("login.gtpl")
	        w.Header().Set("Content-Type", "text/html")
	        t.Execute(w, sess.Get("username"))
	    } else {
	        sess.Set("username", r.Form["username"])
	        http.Redirect(w, r, "/", 302)
	    }
	}

Operaciones con valores: definir, obtener y eliminar.

La función SessionStart retorna una variable que implementa la interfaz de sesión. ¿Cómo la usamos?

Viste un session.Get("uid") en el ejemplo anterior para una operación básica. Ahora examinémolo en mejor detalle.

	func count(w http.ResponseWriter, r *http.Request) {
	    sess := globalSessions.SessionStart(w, r)
	    createtime := sess.Get("createtime")
	    if createtime == nil {
	        sess.Set("createtime", time.Now().Unix())
	    } else if (createtime.(int64) + 360) < (time.Now().Unix()) {
	        globalSessions.SessionDestroy(w, r)
	        sess = globalSessions.SessionStart(w, r)
	    }
	    ct := sess.Get("countnum")
	    if ct == nil {
	        sess.Set("countnum", 1)
	    } else {
	        sess.Set("countnum", (ct.(int) + 1))
	    }
	    t, _ := template.ParseFiles("count.gtpl")
	    w.Header().Set("Content-Type", "text/html")
	    t.Execute(w, sess.Get("countnum"))
	}

Como puedes ver, operar con sesiones simplemente es usar un patrón llave/valor en las operaciones de definición, obtención y eliminación.

Porque las sesiones tiene el concepto de tiempo de expiración definimos un Recolector de Basura para actualizar el último tiempo de modificación. De esta manera, el recolector de basura no eliminará sesiones que todavía están siendo usados.

Definir sesiones.

Sabemos que las aplicaciones web tienen una operación de cerrar sesión. Cuando los usuarios cierran sesión, necesitamos eliminar la sesión correspondiente. Ya hemos usado la operación de reset en el ejemplo anterior, ahora vamos a mirar el cuerpo de la función.

	//Destroy sessionid
	func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
	    cookie, err := r.Cookie(manager.cookieName)
	    if err != nil || cookie.Value == "" {
	        return
	    } else {
	        manager.lock.Lock()
	        defer manager.lock.Unlock()
	        manager.provider.SessionDestroy(cookie.Value)
	        expiration := time.Now()
	        cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
	        http.SetCookie(w, &cookie)
	    }
	}

Eliminar sesiones

Vamos a ver como dejar al manejador de sesiones eliminar una sesión. Necesitamos iniciar el recolector de baseura en la función main():

	func init() {
	    go globalSessions.GC()
	}

	func (manager *Manager) GC() {
	    manager.lock.Lock()
	    defer manager.lock.Unlock()
	    manager.provider.SessionGC(manager.maxlifetime)
	    time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
	}

Como podemos ver el recolector de basura hace un uso completo del paquete time. Automáticamente llama al recolector de basura cuando la sesión se termina, asegurando que todas las sesiones se puedan usar durante maxLifeTime. Una solución similar puede usarse para contar los usuarios activos.

Resumen

Hemos implementado un manejador de sesión global para una aplicación web y definido la interfaz Provider como implementación de una Session. En la siguiente sección vamos a hablar sobre como implementar un Provider para una estructura de almacenamiento de sesiones, que podrás referencia en el futuro.

Enlaces