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.
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.
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.
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.
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 criteriomaxLifeTime
.
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
}
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)
}
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)
}
}
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.
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)
}
}
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.
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.
- Índice
- Sección anterior: Sesiones y cookies
- Siguiente sección: Almacenamiento de sesiones