6

He estado buscando una manera de hacer sesiones/autenticación basada en cookies en Google App Engine porque no me gusta la idea de las sesiones basadas en Memcache, y tampoco me gusta la idea de forzar a los usuarios a crear cuentas de google solo para usar un sitio web. Me encontré con el posting de alguien que mencionaba algunas funciones de cookies firmadas del marco Tornado y parece que es lo que necesito. Lo que tengo en mente es almacenar la identificación de un usuario en una cookie inviolable y tal vez usar un decorador para los manejadores de solicitudes para probar el estado de autenticación del usuario, y como beneficio adicional, la identificación del usuario estará disponible para el manejador de solicitudes. trabajo de datastore y tal. El concepto sería similar a la autenticación de formularios en ASP.NET. Este código proviene del módulo web.py del marco Tornado.Google App Engine - Cookies seguras

De acuerdo con los documentos, "Firma y marca la hora de una cookie para que no se pueda falsificar" y "Devuelve la cookie firmada si valida, o Ninguna".

He intentado usarlo en un proyecto de App Engine, pero no entiendo los inconvenientes de intentar que estos métodos funcionen en el contexto del controlador de solicitudes. ¿Puede alguien mostrarme la forma correcta de hacerlo sin perder la funcionalidad que los desarrolladores de FriendFeed le ponen? Las porciones set_secure_cookie y get_secure_cookie son las más importantes, pero sería bueno poder usar los otros métodos también.

#!/usr/bin/env python 

import Cookie 
import base64 
import time 
import hashlib 
import hmac 
import datetime 
import re 
import calendar 
import email.utils 
import logging 

def _utf8(s): 
    if isinstance(s, unicode): 
     return s.encode("utf-8") 
    assert isinstance(s, str) 
    return s 

def _unicode(s): 
    if isinstance(s, str): 
     try: 
      return s.decode("utf-8") 
     except UnicodeDecodeError: 
      raise HTTPError(400, "Non-utf8 argument") 
    assert isinstance(s, unicode) 
    return s 

def _time_independent_equals(a, b): 
    if len(a) != len(b): 
     return False 
    result = 0 
    for x, y in zip(a, b): 
     result |= ord(x)^ord(y) 
    return result == 0 

def cookies(self): 
    """A dictionary of Cookie.Morsel objects.""" 
    if not hasattr(self,"_cookies"): 
     self._cookies = Cookie.BaseCookie() 
     if "Cookie" in self.request.headers: 
      try: 
       self._cookies.load(self.request.headers["Cookie"]) 
      except: 
       self.clear_all_cookies() 
    return self._cookies 

def _cookie_signature(self,*parts): 
    self.require_setting("cookie_secret","secure cookies") 
    hash = hmac.new(self.application.settings["cookie_secret"], 
        digestmod=hashlib.sha1) 
    for part in parts:hash.update(part) 
    return hash.hexdigest() 

def get_cookie(self,name,default=None): 
    """Gets the value of the cookie with the given name,else default.""" 
    if name in self.cookies: 
     return self.cookies[name].value 
    return default 

def set_cookie(self,name,value,domain=None,expires=None,path="/", 
       expires_days=None): 
    """Sets the given cookie name/value with the given options.""" 
    name = _utf8(name) 
    value = _utf8(value) 
    if re.search(r"[\x00-\x20]",name + value): 
     # Don't let us accidentally inject bad stuff 
     raise ValueError("Invalid cookie %r:%r" % (name,value)) 
    if not hasattr(self,"_new_cookies"): 
     self._new_cookies = [] 
    new_cookie = Cookie.BaseCookie() 
    self._new_cookies.append(new_cookie) 
    new_cookie[name] = value 
    if domain: 
     new_cookie[name]["domain"] = domain 
    if expires_days is not None and not expires: 
     expires = datetime.datetime.utcnow() + datetime.timedelta(
      days=expires_days) 
    if expires: 
     timestamp = calendar.timegm(expires.utctimetuple()) 
     new_cookie[name]["expires"] = email.utils.formatdate(
      timestamp,localtime=False,usegmt=True) 
    if path: 
     new_cookie[name]["path"] = path 

def clear_cookie(self,name,path="/",domain=None): 
    """Deletes the cookie with the given name.""" 
    expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) 
    self.set_cookie(name,value="",path=path,expires=expires, 
        domain=domain) 

def clear_all_cookies(self): 
    """Deletes all the cookies the user sent with this request.""" 
    for name in self.cookies.iterkeys(): 
     self.clear_cookie(name) 

def set_secure_cookie(self,name,value,expires_days=30,**kwargs): 
    """Signs and timestamps a cookie so it cannot be forged""" 
    timestamp = str(int(time.time())) 
    value = base64.b64encode(value) 
    signature = self._cookie_signature(name,value,timestamp) 
    value = "|".join([value,timestamp,signature]) 
    self.set_cookie(name,value,expires_days=expires_days,**kwargs) 

def get_secure_cookie(self,name,include_name=True,value=None): 
    """Returns the given signed cookie if it validates,or None""" 
    if value is None:value = self.get_cookie(name) 
    if not value:return None 
    parts = value.split("|") 
    if len(parts) != 3:return None 
    if include_name: 
     signature = self._cookie_signature(name,parts[0],parts[1]) 
    else: 
     signature = self._cookie_signature(parts[0],parts[1]) 
    if not _time_independent_equals(parts[2],signature): 
     logging.warning("Invalid cookie signature %r",value) 
     return None 
    timestamp = int(parts[1]) 
    if timestamp < time.time() - 31 * 86400: 
     logging.warning("Expired cookie %r",value) 
     return None 
    try: 
     return base64.b64decode(parts[0]) 
    except: 
     return None 

uid = 1234 | 1234567890 | d32b9e9c67274fa062e2599fd659cc14

Partes:
1. UID es el nombre de la clave
2. 1234 es su valor en clara
3. 1234567890 es la marca de tiempo
4. d32b9e9c67274fa062e2599fd659cc14 es la firma hecha a partir del valor y la marca de tiempo

Respuesta

3

Esto funciona si alguien está interesado:

from google.appengine.ext import webapp 

import Cookie 
import base64 
import time 
import hashlib 
import hmac 
import datetime 
import re 
import calendar 
import email.utils 
import logging 

def _utf8(s): 
    if isinstance(s, unicode): 
     return s.encode("utf-8") 
    assert isinstance(s, str) 
    return s 

def _unicode(s): 
    if isinstance(s, str): 
     try: 
      return s.decode("utf-8") 
     except UnicodeDecodeError: 
      raise HTTPError(400, "Non-utf8 argument") 
    assert isinstance(s, unicode) 
    return s 

def _time_independent_equals(a, b): 
    if len(a) != len(b): 
     return False 
    result = 0 
    for x, y in zip(a, b): 
     result |= ord(x)^ord(y) 
    return result == 0 


class ExtendedRequestHandler(webapp.RequestHandler): 
    """Extends the Google App Engine webapp.RequestHandler.""" 
    def clear_cookie(self,name,path="/",domain=None): 
     """Deletes the cookie with the given name.""" 
     expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) 
     self.set_cookie(name,value="",path=path,expires=expires, 
         domain=domain)  

    def clear_all_cookies(self): 
     """Deletes all the cookies the user sent with this request.""" 
     for name in self.cookies.iterkeys(): 
      self.clear_cookie(name)    

    def cookies(self): 
     """A dictionary of Cookie.Morsel objects.""" 
     if not hasattr(self,"_cookies"): 
      self._cookies = Cookie.BaseCookie() 
      if "Cookie" in self.request.headers: 
       try: 
        self._cookies.load(self.request.headers["Cookie"]) 
       except: 
        self.clear_all_cookies() 
     return self._cookies 

    def _cookie_signature(self,*parts): 
     """Hashes a string based on a pass-phrase.""" 
     hash = hmac.new("MySecretPhrase",digestmod=hashlib.sha1) 
     for part in parts:hash.update(part) 
     return hash.hexdigest() 

    def get_cookie(self,name,default=None): 
     """Gets the value of the cookie with the given name,else default.""" 
     if name in self.request.cookies: 
      return self.request.cookies[name] 
     return default 

    def set_cookie(self,name,value,domain=None,expires=None,path="/",expires_days=None): 
     """Sets the given cookie name/value with the given options.""" 
     name = _utf8(name) 
     value = _utf8(value) 
     if re.search(r"[\x00-\x20]",name + value): # Don't let us accidentally inject bad stuff 
      raise ValueError("Invalid cookie %r:%r" % (name,value)) 
     new_cookie = Cookie.BaseCookie() 
     new_cookie[name] = value 
     if domain: 
      new_cookie[name]["domain"] = domain 
     if expires_days is not None and not expires: 
      expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days) 
     if expires: 
      timestamp = calendar.timegm(expires.utctimetuple()) 
      new_cookie[name]["expires"] = email.utils.formatdate(timestamp,localtime=False,usegmt=True) 
     if path: 
      new_cookie[name]["path"] = path 
     for morsel in new_cookie.values(): 
      self.response.headers.add_header('Set-Cookie',morsel.OutputString(None)) 

    def set_secure_cookie(self,name,value,expires_days=30,**kwargs): 
     """Signs and timestamps a cookie so it cannot be forged""" 
     timestamp = str(int(time.time())) 
     value = base64.b64encode(value) 
     signature = self._cookie_signature(name,value,timestamp) 
     value = "|".join([value,timestamp,signature]) 
     self.set_cookie(name,value,expires_days=expires_days,**kwargs) 

    def get_secure_cookie(self,name,include_name=True,value=None): 
     """Returns the given signed cookie if it validates,or None""" 
     if value is None:value = self.get_cookie(name) 
     if not value:return None 
     parts = value.split("|") 
     if len(parts) != 3:return None 
     if include_name: 
      signature = self._cookie_signature(name,parts[0],parts[1]) 
     else: 
      signature = self._cookie_signature(parts[0],parts[1]) 
     if not _time_independent_equals(parts[2],signature): 
      logging.warning("Invalid cookie signature %r",value) 
      return None 
     timestamp = int(parts[1]) 
     if timestamp < time.time() - 31 * 86400: 
      logging.warning("Expired cookie %r",value) 
      return None 
     try: 
      return base64.b64decode(parts[0]) 
     except: 
      return None 

Se puede utilizar la siguiente manera:

class MyHandler(ExtendedRequestHandler): 
    def get(self): 
     self.set_cookie(name="MyCookie",value="NewValue",expires_days=10) 
     self.set_secure_cookie(name="MySecureCookie",value="SecureValue",expires_days=10) 

     value1 = self.get_cookie('MyCookie') 
     value2 = self.get_secure_cookie('MySecureCookie') 
12

Tornado nunca fue pensado para trabajar con ingenio h App Engine (es "su propio servidor" de principio a fin). ¿Por qué no elige en su lugar un marco que era destinado a App Engine de la palabra "ir" y es ligero y elegante, como tipfy? Le proporciona autenticación utilizando su propio sistema de usuario o cualquiera de los propios users de App Engine, OpenIn, OAuth y Facebook; sesiones con cookies seguras o datastore GAE; y mucho más, todo en un enfoque "sin marco" extremadamente ligero basado en WSGI y Werkzeug. ¡¿Que es no gustar?!

+1

yo no tenía la intención de utilizar Tornado con App Engine, sólo quiero fijar a buscar las cookies firmadas de la forma en que lo hicieron. Eché un vistazo al código de cookie seguro tipfy/werkzeug y creo que lo que están haciendo en Tornado es más elegante. – tponthieux

0

Si solo desea almacenar la identificación del usuario en la cookie (probablemente para que pueda ver su registro en el almacén de datos), no necesita cookies 'seguras' o inviolables, solo necesita un espacio de nombres eso es lo suficientemente grande como para hacer que adivinar las ID de usuario no sea práctico, por ejemplo, GUID u otros datos aleatorios.

Una opción prefabricada para esto, que utiliza el almacén de datos para el almacenamiento de la sesión, es Beaker. Alternativamente, puede manejar esto usted mismo con set-cookie/headers de cookies, si realmente solo necesita almacenar su ID de usuario.

+0

Almacenar la identificación del usuario en una cookie no es un problema, pero eso no es todo lo que busco. Los GUID de los motores de aplicaciones no son imprácticos de adivinar, y el uso de algunos otros GUID para autenticar a un usuario parece ser mucho más problemático de lo que vale. Tener la identificación del usuario en una cookie firmada resolvería el problema muy bien siempre que el algoritmo de hash se ejecute razonablemente rápido. Miré a Beaker varias veces antes y decidí no hacerlo porque no se parecía a lo que quería. – tponthieux

+0

Estoy seguro de que alguien verá esto y sabrá exactamente cómo hacer que el código de Tornado funcione. Se muestra fuera de contexto en la publicación de la pregunta, pero los fragmentos de código están destinados a ser parte del controlador de solicitudes de Tornado. Intenté extender el manejador de solicitudes de aplicaciones web, pero no pude hacerlo funcionar. La solución es probablemente algo simple, pero necesito a alguien con más experiencia para mostrarme cómo hacerlo. – tponthieux

+0

Tengo curiosidad de por qué está tan decidido a usar el módulo de sesión de Tornado? Hay varios otros buenos módulos de sesión, incluido Beaker, que proporciona una opción firmada solo de cookies. –

0

Alguien extrajo recientemente el código de autenticación y sesión de Tornado y creó una nueva biblioteca específicamente para GAE.

Quizás esto es más de lo que necesita, pero como lo hicieron específicamente para GAE, no debería tener que preocuparse de adaptarlo usted mismo.

Su biblioteca se llama gaema.Aquí está su anuncio en el grupo GAE Python el 4 mar 2010: http://groups.google.com/group/google-appengine-python/browse_thread/thread/d2d6c597d66ecad3/06c6dc49cb8eca0c?lnk=gst&q=tornado#06c6dc49cb8eca0c

+0

Esto es muy bueno. El framework webapp debería agregar soporte para mecanismos de autenticación de terceros ya que parece ser una tendencia popular. Esto es lo que dicen en la página de gaema "gaema solo autentica a un usuario y no proporciona persistencia como sesiones o cookies seguras para mantener al usuario conectado ..." – tponthieux

3

Para aquellos que todavía están buscando, hemos extraído sólo la aplicación de la galleta del tornado que se puede utilizar con App Engine en ThriveSmart. Lo estamos utilizando con éxito en App Engine y continuaremos manteniéndolo actualizado.

la propia biblioteca cookie es en: http://github.com/thrivesmart/prayls/blob/master/prayls/lilcookies.py

Puede verlo en acción en nuestra aplicación de ejemplo que se incluye. Si la estructura de nuestro repositorio cambia alguna vez, puede buscar lilcookes.py dentro de github.com/thrivesmart/prayls

¡Espero que sea útil para alguien por ahí!