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

我希望這對那裏的人有幫助!