Google App Engine - Безопасные файлы cookie - PullRequest
6 голосов
/ 28 марта 2010

Я искал способ сделать аутентификацию / сеансы на основе файлов cookie в Google App Engine, потому что мне не нравится идея сеансов на основе memcache, а также мне не нравится идея заставлять пользователей создавать Google счета только для использования веб-сайта. Я наткнулся на чью-то публикацию , в которой упоминаются некоторые подписанные функции cookie из фреймворка Tornado, и похоже, что мне нужно. Я имею в виду сохранение идентификатора пользователя в cookie-файле с защитой от несанкционированного доступа и, возможно, использование декоратора для обработчиков запросов для проверки статуса аутентификации пользователя, а в качестве дополнительного преимущества идентификатор пользователя будет доступен для обработчика запросов для работа с хранилищем данных и тому подобное. Концепция будет аналогична аутентификации форм в ASP.NET. Этот код взят из модуля web.py платформы Tornado.

В соответствии с документами, он "подписывает и ставит отметку времени о 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 - подпись, сделанная из значения и отметки времени

Ответы [ 5 ]

12 голосов
/ 28 марта 2010

Tornado никогда не предназначался для работы с App Engine (это «собственный сервер»). Почему бы вам не выбрать вместо этого какой-то фреймворк, который был предназначен для App Engine от слова "go" и является легковесным и стильным, например tipfy ? Он предоставляет вам аутентификацию, используя свою собственную пользовательскую систему или любую из собственных приложений App Engine users, OpenIn, OAuth и Facebook; сеансы с безопасными файлами cookie или хранилищем данных GAE; и многое другое, кроме того, все в сверхлегком «некаркасном» подходе, основанном на WSGI и Werkzeug. Что не нравится?!

3 голосов
/ 25 июля 2010

Это работает, если кому-то интересно:

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')
3 голосов
/ 19 июня 2010

Для тех, кто все еще ищет, мы извлекли только реализацию cookie Tornado, которую вы можете использовать с App Engine на ThriveSmart. Мы успешно используем его в App Engine и будем продолжать обновлять его.

Сама библиотека cookie находится по адресу: http://github.com/thrivesmart/prayls/blob/master/prayls/lilcookies.py

Вы можете увидеть это в действии в нашем примере приложения, которое включено. Если структура нашего хранилища когда-либо изменится, вы можете найти lilcookes.py в github.com/thrivesmart/prayls

Надеюсь, это кому-нибудь пригодится!

0 голосов
/ 29 марта 2010

Кто-то недавно извлек код аутентификации и сеанса из Tornado и создал новую библиотеку специально для GAE.

Возможно, это больше, чем вам нужно, но, поскольку они сделали это специально для GAE, вам не нужно беспокоиться об адаптации самостоятельно.

Их библиотека называется Гема. Вот их объявление в группе GAE Python 4 марта 2010 года: http://groups.google.com/group/google-appengine-python/browse_thread/thread/d2d6c597d66ecad3/06c6dc49cb8eca0c?lnk=gst&q=tornado#06c6dc49cb8eca0c

0 голосов
/ 28 марта 2010

Если вы хотите сохранить только идентификатор пользователя в файле cookie (предположительно, чтобы вы могли просмотреть его запись в хранилище данных), вам не нужны «безопасные» или защищенные от несанкционированного доступа файлы cookie - вам просто нужно пространство имен достаточно большой, чтобы нецелесообразно угадывать идентификаторы пользователей - например, GUID или другие случайные данные.

Один из заранее подготовленных вариантов для этого, который использует хранилище данных для хранения сеанса, - Стакан . С другой стороны, вы можете справиться с этим самостоятельно с помощью заголовков set-cookie / cookie, если вам действительно нужно сохранить их идентификатор пользователя.

...