【问题标题】:Google App Engine - Secure CookiesGoogle App Engine - 安全 Cookie
【发布时间】:2011-02-01 16:27:08
【问题描述】:

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

根据文档字符串,它“对 cookie 进行签名和时间戳记,因此无法伪造”和 "如果验证通过,则返回给定的签名 cookie,否则返回 None。"

我曾尝试在 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是key的名字
2. 1234 是你的明确值
3. 1234567890 是时间戳
4. d32b9e9c67274fa062e2599fd659cc14 是由值和时间戳组成的签名

【问题讨论】:

    标签: google-app-engine session forms-authentication cookies tornado


    【解决方案1】:

    如果有人感兴趣,这可行:

    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')
    

    【讨论】:

      【解决方案2】:

      对于仍在寻找的人,我们仅提取了 Tornado cookie 实现,您可以在 ThriveSmart 与 App Engine 一起使用。我们已在 App Engine 上成功使用它,并将继续保持更新。

      cookie 库本身位于: http://github.com/thrivesmart/prayls/blob/master/prayls/lilcookies.py

      您可以在我们包含的示例应用中看到它的实际效果。如果我们存储库的结构发生变化,您可以在 github.com/thrivesmart/prayls 中查找 lilcookes.py

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

      【讨论】:

        【解决方案3】:

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

        也许这比你需要的更多,但由于他们是专门为 GAE 做的,你不必担心自己调整它。

        他们的图书馆叫做 gaema。以下是他们在 2010 年 3 月 4 日在 GAE Python 小组中的公告: http://groups.google.com/group/google-appengine-python/browse_thread/thread/d2d6c597d66ecad3/06c6dc49cb8eca0c?lnk=gst&q=tornado#06c6dc49cb8eca0c

        【讨论】:

        • 这很酷。 webapp 框架应该添加对第三方身份验证机制的支持,因为这似乎是一种流行趋势。这是他们在 gaema 页面上所说的“gaema 仅对用户进行身份验证,不提供会话或安全 cookie 等持久性来保持用户登录......”
        【解决方案4】:

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

        为此使用数据存储进行会话存储的一个预制选项是Beaker。或者,如果您真的只需要存储他们的用户 ID,您可以使用 set-cookie/cookie 标头自己处理。

        【讨论】:

        • 在 cookie 中存储用户的 id 不是问题,但这不是我所追求的。猜测应用引擎 GUID 并非不切实际,而且使用其他一些 GUID 来验证用户的身份似乎比它的价值要麻烦得多。只要散列算法运行得相当快,将用户的 id 放在签名的 cookie 中就可以很好地解决问题。我之前已经看过 Beaker 好几次了,因为它看起来不像我想要的。
        • 我相信有人会看到这一点并且确切地知道如何使 Tornado 中的代码工作。它在问题发布中显示为断章取义,但代码 sn-ps 旨在成为 Tornado 请求处理程序的一部分。我尝试扩展 webapp 请求处理程序,但我无法让它工作。修复可能很简单,但我需要有更多经验的人来告诉我如何去做。
        • 我很好奇你为什么如此坚定地使用 Tornado 的会话模块?还有其他几个不错的会话模块,包括 Beaker,它提供了一个仅签名的 cookie 选项。
        【解决方案5】:

        Tornado 从未打算与 App Engine 一起使用(它始终是“自己的服务器”)。你为什么不从“go”这个词中选择一些用于 App Engine 的框架,并且是轻量级和花哨的,例如 tipfy?它使用自己的用户系统或 App Engine 自己的users、OpenIn、OAuth 和 Facebook 中的任何一个为您提供身份验证;具有安全 cookie 或 GAE 数据存储的会话;除此之外,所有这些都采用基于 WSGI 和 Werkzeug 的超轻量级“非框架”方法。有什么不喜欢的?!

        【讨论】:

        • 我不打算将 Tornado 与 App Engine 一起使用,我只是想按照他们的方式设置和获取签名的 cookie。我查看了 tipfy/werkzeug 安全 cookie 代码,我认为他们在 Tornado 中所做的更优雅。
        猜你喜欢
        • 1970-01-01
        • 2011-01-21
        • 2013-05-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多