Google App Engine — безопасные файлы cookie

Я искал способ выполнить аутентификацию / сеансы на основе файлов 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 — подпись, состоящая из значения и отметки времени


person tponthieux    schedule 28.03.2010    source источник


Ответы (5)


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

person Alex Martelli    schedule 28.03.2010
comment
Я не собирался использовать Tornado с App Engine, я просто хочу устанавливать и получать подписанные файлы cookie, как это делали они. Я взглянул на безопасный код cookie tipfy/werkzeug и думаю, что то, что они делают в Tornado, более элегантно. - person tponthieux; 28.03.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.

Я надеюсь, что это полезно для кого-то там!

person matt    schedule 19.06.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')
person tponthieux    schedule 25.07.2010

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

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

person Nick Johnson    schedule 28.03.2010
comment
Сохранение идентификатора пользователя в файле cookie не проблема, но это еще не все, что мне нужно. Угадать GUID движка приложений нетрудно, и использование какого-либо другого GUID для аутентификации пользователя кажется гораздо более проблематичным, чем оно того стоит. Наличие идентификатора пользователя в подписанном файле cookie прекрасно решит проблему, если алгоритм хеширования работает достаточно быстро. Я уже несколько раз смотрел на Beaker и отказывался от него, потому что он не был похож на то, что я хотел. - person tponthieux; 29.03.2010
comment
Я уверен, что кто-то увидит это и точно знает, как заставить работать код от Tornado. Это показано вне контекста в сообщении вопроса, но фрагменты кода предназначены для того, чтобы быть частью обработчика запросов Tornado. Я попытался расширить обработчик запросов веб-приложения, но не смог заставить его работать. Исправление, вероятно, что-то простое, но мне нужен кто-то с большим опытом, чтобы показать мне, как это сделать. - person tponthieux; 29.03.2010
comment
Мне любопытно, почему вы так решительно настроены использовать сеансовый модуль Tornado? Есть несколько других хороших модулей сеанса, в том числе Beaker, который предоставляет опцию только для подписанных файлов cookie. - person Nick Johnson; 29.03.2010

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

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

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

person Tom    schedule 29.03.2010
comment
Это довольно круто. Фреймворк веб-приложений должен добавить поддержку сторонних механизмов аутентификации, поскольку это стало популярной тенденцией. Вот что они говорят на странице gaema gaema только аутентифицирует пользователя и не обеспечивает постоянства, такого как сеансы или безопасные файлы cookie, чтобы пользователь оставался в системе. - person tponthieux; 29.03.2010