首页 > Python > django不同sites间的session共享
201302月21

django不同sites间的session共享

环境

Django==1.4.3

起因

在同一域名下的不同目录挂了两个django项目,类似:site1(url.com/a/),sites2(url.com/b/),发现登陆其中一个站点,另一个站点就会退出登陆。原因其实也不是很难猜,应该是同域下的cookie共享造成的,而我两个项目中的SESSION_COOKIE_NAME配置又是一样的,同时后端session数据又存储在同一个地方。但这个结果却不是很让我理解,因为如果所有都是一样的话,最后的结果应该是登陆一个site,另一个site也自动登陆,退出一个site,两个sites同时退出。

原理

看了自己的代码和django的session实现,终于发现了问题所在,下面就逐步剖析一下造成这个结果的罪魁祸首。
我们先从用户登陆说起,因为django自带auth模块,所以直接使用其中的login方法来登陆。

# django/contrib/auth/__init__.py
def login(request, user):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    if user is None:
        user = request.user
    # TODO: It would be nice to support different login methods, like signed cookies.
    if SESSION_KEY in request.session:
        if request.session[SESSION_KEY] != user.id:
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()
    request.session[SESSION_KEY] = user.id
    request.session[BACKEND_SESSION_KEY] = user.backend
    if hasattr(request, 'user'):
        request.user = user
    user_logged_in.send(sender=user.__class__, request=request, user=user)

      明显的这个方法是依赖request.session的,所以我们再看看request安装session的地方

# django/contrib/sessions/middleware.py
class SessionMiddleware(object):
    def process_request(self, request):
        engine = import_module(settings.SESSION_ENGINE)
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
        request.session = engine.SessionStore(session_key)

看来是根据SESSION_COOKIE_NAME来生成session对象的,下面是SessionStore类

class SessionStore(SessionBase):
    """
    A Tokyo Cabinet-based session store.
    """
    def load(self):
        try:
            session_data = server.get(self.session_key)
        except:
            server = pytyrant.PyTyrant.open(TT_HOST, TT_PORT)
            session_data = server.get(self.session_key)
        if session_data is not None:
            expiry, data = int(session_data[:15]), session_data[15:]
            if expiry < time.time():
                return {}
            else:
                return self.decode(force_unicode(data))
        self.create()
        return {}

    def create(self):
        while True:
            self._session_key = self._get_new_session_key()
            try:
                self.save(must_create=True)
            except CreateError:
                continue
            self.modified = True
            return

    def save(self, must_create=False):
        if must_create and self.exists(self.session_key):
            raise CreateError
        data = self.encode(self._get_session(no_load=must_create))
        encoded = '%15d%s' % (int(time.time()) + self.get_expiry_age(), data)
        server[self._get_or_create_session_key()] = encoded

    def exists(self, session_key):
        retrieved = server.get(session_key)
        if retrieved is None:
            return False
        expiry, data = int(retrieved[:15]), retrieved[15:]
        if expiry < time.time():
            return False
        return True

    def delete(self, session_key=None):
        if session_key is None:
            if self._session_key is None:
                return
            session_key = self._session_key
        del server[session_key]

所以用户每次登陆都会从cookie中获取SESSION_COOKIE_NAME,然后根据SESSION_COOKIE_NAME的值来生成session对象,如果SESSION_COOKIE_NAME就生成一个新的session。
那它是怎么判断当前用户是已登陆还是未登陆呢,这就要看AuthenticationMiddleware这个中间件了,这里面会给request安装user对象,安装前会对user做相关判断

# django/contrib/auth/__init__.py
def get_user(request):
    from django.contrib.auth.models import AnonymousUser
    try:
        user_id = request.session[SESSION_KEY]
        backend_path = request.session[BACKEND_SESSION_KEY]
        backend = load_backend(backend_path)
        user = backend.get_user(user_id) or AnonymousUser()
    except KeyError:
        user = AnonymousUser()
    return user

此时和判断session中是否存在SESSION_KEY,BACKEND_SESSION_KEY,[]会调用session中的__getitem__方法

# django/contrib/auth/__init__.py
def __getitem__(self, key):
        return self._session[key]

def _get_session(self, no_load=False):
        """
        Lazily loads session from storage (unless "no_load" is True, when only
        an empty dict is stored) and stores it in the current instance.
        """
        self.accessed = True
        try:
            return self._session_cache
        except AttributeError:
            if self.session_key is None or no_load:
                self._session_cache = {}
            else:
                self._session_cache = self.load()
        return self._session_cache

_session = property(_get_session)

def _hash(self, value):
    key_salt = "django.contrib.sessions" + self.__class__.__name__
    return salted_hmac(key_salt, value).hexdigest()

def encode(self, session_dict):
    "Returns the given session dictionary pickled and encoded as a string."
    pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
    hash = self._hash(pickled)
    return base64.encodestring(hash + ":" + pickled)

def decode(self, session_data):
    encoded_data = base64.decodestring(session_data)
    try:
        # could produce ValueError if there is no ':'
        hash, pickled = encoded_data.split(':', 1)
        expected_hash = self._hash(pickled)
        if not constant_time_compare(hash, expected_hash):
            raise SuspiciousOperation("Session data corrupted")
        else:
            return pickle.loads(pickled)
    except Exception:
        # ValueError, SuspiciousOperation, unpickling exceptions. If any of
        # these happen, just return an empty dictionary (an empty session).
        return {}

def salted_hmac(key_salt, value, secret=None):
    """
    Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a
    secret (which defaults to settings.SECRET_KEY).

    A different key_salt should be passed in for every application of HMAC.
    """
    if secret is None:
        secret = settings.SECRET_KEY

    # We need to generate a derived key from our base key.  We can do this by
    # passing the key_salt and our base key through a pseudo-random function and
    # SHA1 works nicely.
    key = hashlib.sha1(key_salt + secret).digest()

    # If len(key_salt + secret) > sha_constructor().block_size, the above
    # line is redundant and could be replaced by key = key_salt + secret, since
    # the hmac module does the same thing for keys longer than the block size.
    # However, we need to ensure that we *always* do this.
    return hmac.new(key, msg=value, digestmod=hashlib.sha1)

从上面就能看出来,session的数据是调用load方法获取的,我们返回看一下SessionStore类中的load方法,里面调用了decode来解码存入数据库中的session数据,我们看看session数据是怎么生成的,看看encode方法,先pickle,在将pickle的值进行hash,然后以’:‘分隔,做base64编码。
这时候就知道原来是因为两个sites生成的hash值不一样,所以无法登陆,它的秘密都在salted_hmac这个方法中,原来每个hash值都会加入当前site的settings.SECRET_KEY,所以不同sites是无法通过constant_time_compare(hash, expected_hash)这个验证的,如果要达到先前说的想法,登陆一个site,另一个也能自动登陆,就需要将两个sites的settings.SECRET_KEY设置为一样的。

文章作者: iitshare
本文地址:http://www.iitshare.com/django-sites-session-share.html
版权所有 © 转载时必须以链接形式注明作者和原始出处!

更多
本文目前尚无任何评论.

发表评论