simplesession: Gestión de sesiones anónimas para Google App Engine
Descarga
25/04/2008 - simplesession v0.1
Desarrollo
En el momento de escribir este artículo (24/04/2008), la plataforma Google App Engine no permite la gestión de sesiones en las aplicaciones hospedadas si no es utilizando el API específico de gestión de usuarios de Google.
Esto tiene dos implicaciones:
- Para realizar el seguimiento de las variables de sesión, los usuarios que utilicen tu aplicación deben ser usuarios de Google y estar registrados.
- Si en algún momento quieres migrar tu aplicación a otro entorno, necesitarás eliminar todas las referencias al API de usuarios que proporciona Google.
Para algunos proyectos que estoy realizando, me interesaba contar con un sistema de gestión de sesiones anónimas que me permitiera controlar el estado de la aplicación sin necesidad de depender de que los usuarios tuviesen cuenta en Google.
Además, a partir de un sistema de esas características, sería relativamente sencillo implementar mi propio sistema de cuentas de usuario completamente independiente del API de Google, lo que me daría tranquilidad a la hora de realizar alguna migración y no obligaría a los usuarios a crearse cuentas en Google.
Como en muchas otras ocasiones, necesito tenerlo listo en los próximos 10 minutos. Así que hay que ponerse…
Manos a la obra
Aunque el objetivo es realizar un sistema independiente del API de Google, es evidente que necesitamos un método para guardar de manera persistente la información entre sesiones. La única manera que tenemos de hacerlo es utilizando BigTable.
class SessionData (db.Expando): sessid = db.StringProperty (required=True) created = db.DateTimeProperty (auto_now_add=True) expire_date = db.DateTimeProperty ()
¿Por qué he derivado db.Expando en vez de db.Model? Sencillo; necesitamos un objeto capaz de almacenar un número variable de valores y esto es algo que únicamente nos permite la clase db.Expando.
Además del almacenamiento en BigTable, necesitaremos una clase que implemente toda la funcionalidad que esperamos de un objeto “sesión”. En este caso…
class Session (object): def __init__ (self, request_handler): global q_session_data_select def create (): """Creates the *current* session""" global SESSION_CLEANUP_LIMIT global SESSID_LENGTH global g_session_count # We need a new sessid and the corresponding SessionData self.sessid = "".join ([choice (ascii_letters + digits) \ for i in range(SESSID_LENGTH)]) self.__data = SessionData (sessid=self.sessid) self.__data.update () # Writing the cookie now, we don't forget later :) self.handler.response.headers["Set-Cookie"] = \ "sessid=%s; Path=/;" % self.sessid # Do cleanup? g_session_count += 1 if g_session_count % SESSION_CLEANUP_LIMIT == 0: session_cleanup () self.handler = request_handler if request_handler.request.cookies.has_key ("sessid"): self.sessid = request_handler.request.cookies["sessid"] else: self.sessid = "" if len (self.sessid) == 0: create () else: q_session_data_select.bind (self.sessid) self.__data = q_session_data_select.get () if self.__data == None: create () else: if self.__data.expire_date < datetime.now (): create () else: self.__data.update () def put (self): """Saves session data""" if self.__data != None: self.__data.put () def delete (self): """Deletes session data and client cookie""" self.handler.response.headers.add_header ("Set-Cookie", None) if self.__data != None: try: self.__data.delete () except: pass self.__data = None def __setitem__ (self, key, value): setattr (self.__data, key, value) def __getitem__ (self, key): if hasattr (self.__data, key): return getattr (self.__data, key) else: return "" def __delitem__ (self, key): delattr (self.__data, key) def value_names (self): """Returns a list of all values stored in session object""" return self.__data.dynamic_properties ()
No voy a comentar cada línea de código porque su propia lectura da toda la información que se necesita para comenzar a usarlo. Únicamente comentar algunos detalles sobre la clase.
- Para la creación de la clase, debemos pasarle el objeto RequestHandler que va a utilizar el objeto Sesion. Sigue leyendo para saber por qué…
- La clase carga automáticamente el SessionData correspondiente tras leer el valor de la cookie “sessid”
- Si la cookie no existe, crea un nuevo objeto SessionData y escribe el nuevo “sessid” en la cookie al instante.
Además, hace uso de algunas variables globales (se hacen globales para que el sistema las cachee y no penalizar la ejecución)
# Module configuration # Length for session ID (cookie value) SESSID_LENGTH = 64 # Max age MAX_SESSION_AGE = timedelta (minutes=15) # Cleanup expired sessions every SESSION_CLEANUP_LIMIT new sessions SESSION_CLEANUP_LIMIT = 500 # We do not want to check SessionData.all().count() every time, do we? g_session_count = 0 # Let's cache some query objects! q_session_data_select = \ db.GqlQuery ("select * from SessionData where sessid = :1") q_session_data_cleanup = \ db.GqlQuery ("select * from SessionData where expore_date < :1")
…y hacemos uso de la siguiente función para realizar la limpieza de sesiones muertas:
def session_cleanup (): """Executes the cleanup of all expired sessions""" global q_session_data_cleanup q_session_data_cleanup.bind (datetime.now ()) for data in q_session_data_cleanup: data.delete ()
Conclusión
Como más de uno habrá visto, la clase se apodera de la cabecera “Set-Cookie” que enviamos al cliente. A mí personalmente no me preocupa, porque soy contrario al uso de cookies en las aplicaciones (y así lo recomiendo siempre que me preguntan). Si alguien necesita usar cookies de manera arbitraria, siempre podrá mejorar la clase :)
Un ejemplo
Y para finalizar, un pequeño ejemplo del uso de la clase:
import wsgiref.handlers from google.appengine.ext import webapp from simplesession import Session class MainHandler (webapp.RequestHandler): def get (self): session = Session (self) if session["counter"] == "": session["counter"] = 0 else: session["counter"] += 1 # Delete session if counter is greater than 10 if session["counter"] > 10: session.delete () # Save session (if not deleted) session.put () self.response.out.write ("Session counter = %s" % session["counter"]) def main (): application = webapp.WSGIApplication ([\ ('/.*', MainHandler) ], debug=True) wsgiref.handlers.CGIHandler ().run (application) if __name__ == '__main__': main ()
