在Django应用程序中,每个用户只允许单个活动会话

23
我想限制已登录的用户仅拥有一个活动会话,即如果用户使用新的sessionid登录,则旧的会话应该被终止。 我已经在stackoverflow上找到了很多帮助: 这里这里 我实现了中间件解决方案,并进行了一些额外的检查...
class OnlyOneUserMiddleware(object):
"""
Middleware to ensure that a logged-in user only has one session active.
Will kick out any previous session. 
"""
def process_request(self, request):
    if request.user.is_authenticated():
        try:
            cur_session_key = request.user.get_profile().session_key
            if cur_session_key and cur_session_key != request.session.session_key:
                # Default handling... kick the old session...
                Session.objects.get(session_key=cur_session_key).delete()
            if not cur_session_key or cur_session_key != request.session.session_key:
                p = request.user.get_profile()
                p.session_key = request.session.session_key
                p.save()
        except ObjectDoesNotExist:
            pass

目前为止,在Django开发服务器(manage.py runserver)上一切都很好,一切都正常工作,它会清除旧的会话......

但是当使用Apache(带有mod_wsgi)时,它不起作用!

我尝试找到有关此问题的任何信息,但迄今为止没有运气...

我找到的最接近的是这个问题,但它是相反的问题...

非常感谢任何帮助。

编辑:我在删除Session之前添加了一个调试打印语句......下面是来自Apache错误日志的代码片段:

[Fri Jan 20 09:56:50 2012] [error] old key = f42885ccb7f33b6afcb2c18fca14f44a
[Fri Jan 20 09:56:50 2012] [error] new key = ce4cfb672e6025edb8ffcd0cf2b4b8d1
[Fri Jan 20 09:57:14 2012] [error] old key = f42885ccb7f33b6afcb2c18fca14f44a
[Fri Jan 20 09:57:14 2012] [error] new key = 0815c56241ac21cf4b14b326f0aa7e24

前两个错误是我使用第一个会话(Firefox)时出现的。

后两个错误是我使用第二个会话(Chromium)时出现的。

... 结果发现旧的会话记录并未被删除... ???

我正在与与devserver相同的PostgreSQL实例进行比较...

编辑2:结果我的代码有漏洞... 当新的Session_key在Session中找不到时,它失败了...

这是已经修复的代码... 现在try..except在正确的位置上了

class OnlyOneUserMiddleware(object):
    """
    Middleware to ensure that a logged-in user only has one session active.
    Will kick out any previous session. 
    """
    def process_request(self, request):
        if request.user.is_authenticated():
            cur_session_key = request.user.get_profile().session_key
            if cur_session_key and cur_session_key != request.session.session_key:
                # Default handling... kick the old session...
                try:
                    s = Session.objects.get(session_key=cur_session_key)
                    s.delete()
                except ObjectDoesNotExist:
                    pass
            if not cur_session_key or cur_session_key != request.session.session_key:
                p = request.user.get_profile()
                p.session_key = request.session.session_key
                p.save()

4
当你说“不起作用”时,到底是哪个部分出了问题?你仍然在数据库中看到旧会话吗?如果你在Session删除之前加上一个打印/日志调用,你能看到它在mod_wsgi下执行吗? - AdamKG
@AdamKG:感谢您指引我正确的方向! - Mark Johansson
如果会话引擎是cache_db,我认为我们还需要手动从缓存中删除sessionkey,对吗? - Wesley
user.get_profile() 是做什么用的? - gdvalderrama
3个回答

3

虽然网络上有很多类似的问题,但这是我的解决方案。

当用户登录时,遍历所有活动会话并删除具有相同user.id的会话。对于较小的网站,这应该就可以了。

# __init__.py
# Logs user out from all other sessions on login, django 1.8

from django.contrib.sessions.models import Session
from django.contrib.auth.signals import user_logged_in
from django.db.models import Q
from django.utils import timezone

def limit_sessions(sender, user, request, **kwargs):
    # this will be slow for sites with LOTS of active users

    for session in Session.objects.filter(
        ~Q(session_key = request.session.session_key),
        expire_date__gte = timezone.now()
    ):
        data = session.get_decoded()
        if data.get('_auth_user_id', None) == str(user.id):
            # found duplicate session, expire it
            session.expire_date = timezone.now()
            session.save()

    return

user_logged_in.connect(limit_sessions)

1
你比较过这两种方式的性能吗?我的意思是,通过信号和中间件的方式。如果使用中间件,所有请求都应该经过handle_request。如果使用信号,虽然只有登录事件会触发函数,但它将遍历会话表...所以,考虑性能,哪种更好... - Wesley
我确实说过“适用于较小的网站” :) - 你可能想像OP一样存储密钥,但是像我这样只在登录时检查它可以得到更高效的解决方案。 - MarZab
是的,我看到你提到了较小的网站 :-) 我只想知道对于较大的网站来说什么更好 :-) - Wesley

1

虽然不建议使用,但您始终可以采用这种方法,它是有效的。

my_old_sessions = Session.objects.all()
for row in my_old_sessions:
   if row.get_decoded().get("_username") == request.user.username:
      row.delete()

在验证用户之前,您将在login()函数中实现上述代码。

当然,这仅适用于您具有将用户的用户名存储在会话中的login()函数方法,如下所示:

request.session["_username"] = request.user.username

如果您使用这种方法,请记住在进行这些更改后运行服务器之前清空数据库中的所有会话,因为它会引发KeyLookUp错误。


用户名不再是会话对象的get_decoded()中的属性(Django 1.8)。然而,user_id 是存在的,所以 row.get_decoded().get("_auth_user_id") == request.user.pk 是可行的。 - gdvalderrama

0

我觉得,django.contrib.auth信号可能会对此有所帮助。在登录时,使旧的用户会话失效。


如果您已经在使用用户数据库,这意味着所有用户必须先注销才能使此功能生效。 - gdvalderrama

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接