6

我一直在寻找一种方法来在Google App Engine中执行基于cookie的身份验证/会话,因为我不喜欢基于memcache的会话的想法,而且我也不喜欢这个想法强制用户创建Google帐户只是为了使用网站。我偶然发现某人的posting提到了Tornado框架中的一些签名cookie功能,它看起来像我所需要的。我想到的是将用户的ID存储在防篡改cookie中,并且可能使用装饰器来请求处理程序来测试用户的身份验证状态,并且作为副作用,用户ID将可用于请求处理程序数据存储工作等。这个概念与ASP.NET中的表单身份验证类似。这段代码来自Tornado框架的web.py模块。Google App Engine - 安全Cookie

根据文档字符串,它“签名和时间戳一个cookie,以便它不能伪造”和 “返回给定的签名cookie,如果它有效,或无。”

我试过在App Engine项目中使用它,但我不明白试图让这些方法在请求处理程序的上下文中工作的细微差别。有人可以告诉我正确的方式来做到这一点,而不会丢失FriendFeed开发者投入的功能吗? set_secure_cookie和get_secure_cookie部分是最重要的部分,但能够使用其他方法也会很好。

#!/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

配件:
1. UID是其中的关键
2. 1234名是明确
3 1234567890你的价值是时间戳
4. d32b9e9c67274fa062e2599fd659cc14是由数值和时间戳制成的签名

回答

3

这个作品,如果有人有兴趣:

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 

它可以像这样使用:

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从来没有打算工作的机智h App Engine(它是“自己的服务器”贯穿始终)。你为什么不选择一些框架,为App Engine引用的单词“go”,并且是轻量级和花花公子的,比如tipfy?它使用自己的用户系统或任何App Engine自己的users,OpenIn,OAuth和Facebook为您提供身份验证;使用安全cookie或GAE数据存储的会话;除此之外,所有这些都是基于WSGI和Werkzeug的超级轻量级​​“非框架”方法。什么是不喜欢?!

+1

我没打算用旋风App Engine的,我只是想设置和获取以他们的方式签署cookie。我看了一下tipfy/werkzeug安全cookie代码,我认为他们在Tornado做的更优雅。 – tponthieux 2010-03-28 08:52:30

0

如果您只想将用户的用户ID存储在cookie中(大概是这样您可以在数据存储区中查看他们的记录),则不需要“安全”或防篡改cookie - 您只需要一个名称空间这足以让猜测用户ID不切实际 - 例如GUID或其他随机数据。

为此,使用数据存储进行会话存储的一个预制选项是Beaker。或者,如果您确实需要存储其用户ID,则可以使用set-cookie/cookie标题自行处理此问题。

+0

将用户的ID存储在cookie中不是问题,但那不是我所追求的。 App引擎GUID不是不切实际的猜测,并且使用其他一些GUID来验证用户似乎比它的价值更麻烦。只要散列算法合理快速地运行,在已签名的cookie中拥有用户的ID就可以很好地解决问题。我之前几次看过Beaker并决定反对它,因为它看起来不像我想要的。 – tponthieux 2010-03-28 22:09:05

+0

我确定有人会看到这一点,并确切知道如何使Tornado的代码工作。它在问题发布中显示为不在上下文中,但代码片段旨在成为Tornado请求处理程序的一部分。我试图扩展webapp请求处理程序,但我无法让它工作。解决办法可能很简单,但我需要有更多经验的人来告诉我如何去做。 – tponthieux 2010-03-28 22:11:14

+0

我很好奇你为什么如此坚决使用Tornado的会话模块?还有其他几个很好的会话模块,其中包括Beaker,它提供了一个仅限于cookie的选项。 – 2010-03-29 09:13:48

0

最近有人从Tornado中提取认证和会话代码,并为GAE创建了一个新库。

也许这是你需要的更多,但是因为他们专门为GAE做了,所以你不必担心自己适应它。

他们的图书馆被称为gaema。这里是他们的GAE的Python组于2010年3月4日公告: http://groups.google.com/group/google-appengine-python/browse_thread/thread/d2d6c597d66ecad3/06c6dc49cb8eca0c?lnk=gst&q=tornado#06c6dc49cb8eca0c

+0

这很酷。 webapp框架应该增加对第三方认证机制的支持,因为它似乎是一种流行趋势。 以下是他们在gaema页面上说的话:“gaema只验证用户身份,不提供会话持久性或安全cookie以保持用户登录..” – tponthieux 2010-03-29 06:31:42

3

对于那些谁仍然在寻找,我们已经提取只是你可以在ThriveSmart与App Engine使用龙卷风的cookie实现。我们正在App Engine上成功使用它,并将继续保持更新。

该Cookie库本身是: http://github.com/thrivesmart/prayls/blob/master/prayls/lilcookies.py

你可以看到它在包含在我们的示例应用程序的动作。如果我们的资源库的结构发生变化,您可以在github.com/thrivesmart/prayls中查找lilcookes.py

我希望这对那里的人有帮助!