在 Django 中删除特定用户的所有会话的最优化方法?
我正在运行 Django 1.3,使用会话中间件和身份验证中间件:
# settings.py
SESSION_ENGINE = django.contrib.sessions.backends.db # Persist sessions to DB
SESSION_COOKIE_AGE = 1209600 # Cookies last 2 weeks
每次用户从不同位置(不同计算机/浏览器)登录时,都会创建一个新的 Session()
并使用唯一的session_id
。这可能会导致同一用户有多个数据库条目。他们的登录信息将持续保留在该节点上,直到 cookie 被删除或会话过期。
当用户更改密码时,我想从数据库中删除该用户的所有未过期会话。这样,在更改密码后,他们将被迫重新登录。这是出于安全目的,例如,如果您的计算机被盗,或者您不小心在公共终端上保持登录状态。
我想知道优化此问题的最佳方法。这是我的做法:
# sessions_helpers.py
from django.contrib.sessions.models import Session
import datetime
def all_unexpired_sessions_for_user(user):
user_sessions = []
all_sessions = Session.objects.filter(expire_date__gte=datetime.datetime.now())
for session in all_sessions:
session_data = session.get_decoded()
if user.pk == session_data.get('_auth_user_id'):
user_sessions.append(session)
return user_sessions
def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
for session in all_unexpired_sessions_for_user(user):
if session is not session_to_omit:
session.delete()
非常简化的视图:
# views.py
from django.http import HttpResponse
from django.shortcuts import render_to_response
from myapp.forms import ChangePasswordForm
from sessions_helpers import delete_all_unexpired_sessions_for_user
@never_cache
@login_required
def change_password(request):
user = request.user
if request.method == 'POST':
form = ChangePasswordForm(data=request)
if form.is_valid():
user.set_password(form.get('password'))
user.save()
request.session.cycle_key() # Flushes and replaces old key. Prevents replay attacks.
delete_all_unexpired_sessions_for_user(user=user, session_to_omit=request.session)
return HttpResponse('Success!')
else:
form = ChangePasswordForm()
return render_to_response('change_password.html', {'form':form}, context_instance=RequestContext(request))
正如您在 sessions_helpers.py
中看到的,我必须从数据库中提取每个未过期的会话,Session.objects.filter(expire_date__gte=datetime.datetime.now())
,对所有会话进行解码,然后检查它是否与用户匹配。如果数据库中存储了 100,000 多个会话,这对于数据库来说将是极其昂贵的。
有没有一种对数据库更友好的方法来做到这一点?是否有一个 Sessions/Auth 中间件设置可以让您将用户名存储为 Sessions 表中的一列,以便我可以对其运行 SQL,或者我是否必须修改 Sessions 才能做到这一点?开箱即用,它只有 session_key
、session_data
和 expire_date
列。
感谢您提供的任何见解或帮助。 :)
I'm running Django 1.3, using Sessions Middleware and Auth Middleware:
# settings.py
SESSION_ENGINE = django.contrib.sessions.backends.db # Persist sessions to DB
SESSION_COOKIE_AGE = 1209600 # Cookies last 2 weeks
Each time a user logs in from a different location (different computer/browser), a new Session()
is created and saved with a unique session_id
. This can result in multiple database entries for the same user. Their login persists on that node until the cookie is deleted or session expires.
When a user changes their password, I want to delete all unexpired sessions for that user from the DB. That way after a password change, they're forced to re-login. This is for security purposes, such as if your computer got stolen, or you accidentally left yourself logged-in on a public terminal.
I want to know the best way to optimize this. Here's how I've done it:
# sessions_helpers.py
from django.contrib.sessions.models import Session
import datetime
def all_unexpired_sessions_for_user(user):
user_sessions = []
all_sessions = Session.objects.filter(expire_date__gte=datetime.datetime.now())
for session in all_sessions:
session_data = session.get_decoded()
if user.pk == session_data.get('_auth_user_id'):
user_sessions.append(session)
return user_sessions
def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
for session in all_unexpired_sessions_for_user(user):
if session is not session_to_omit:
session.delete()
A very simplified view:
# views.py
from django.http import HttpResponse
from django.shortcuts import render_to_response
from myapp.forms import ChangePasswordForm
from sessions_helpers import delete_all_unexpired_sessions_for_user
@never_cache
@login_required
def change_password(request):
user = request.user
if request.method == 'POST':
form = ChangePasswordForm(data=request)
if form.is_valid():
user.set_password(form.get('password'))
user.save()
request.session.cycle_key() # Flushes and replaces old key. Prevents replay attacks.
delete_all_unexpired_sessions_for_user(user=user, session_to_omit=request.session)
return HttpResponse('Success!')
else:
form = ChangePasswordForm()
return render_to_response('change_password.html', {'form':form}, context_instance=RequestContext(request))
As you can see in sessions_helpers.py
, I have to pull every unexpired session out of the DB, Session.objects.filter(expire_date__gte=datetime.datetime.now())
, decode all of them, and then check to see if it matches a user or not. This will be extremely costly to the database if there are, say, 100,000+ sessions stored in there.
Is there a more-database-friendly way to do this? Is there a Sessions/Auth Middleware setting that'll let you store the username as a column in the Sessions table so I can run SQL against that, or will I have to modify Sessions to do that? Out-of-the-box it only has session_key
, session_data
, and expire_date
columns.
Thanks for any insight or help you can offer. :)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
如果您从
all_unexpired_sessions_for_user
函数返回 QuerySet,则可以将数据库命中次数限制为两次:这将为您提供总共两次数据库命中次数。一次循环遍历所有
Session
对象,一次删除所有会话。不幸的是,我不知道有更直接的方法来过滤会话本身。If you return a QuerySet from your
all_unexpired_sessions_for_user
function, you could limit your database hits to two:This gives you a total of two hits to the database. Once to loop over all of the
Session
objects, and once to delete all of the sessions. Unfortunately, I don't know of a more direct way to filter through the sessions themselves.使用列表理解的函数的另一个版本将直接删除用户的每个未过期会话:
Another version of a function using list comprehension that will just straight up delete every unexpired session of a user:
最有效的方法是在登录期间存储用户的会话 ID。您可以使用 request.session._session_key 访问会话 ID,并将其存储在引用用户的单独模型中。现在,当您想要删除用户的所有会话时,只需查询此模型即可返回相关用户的所有活动会话。现在您只需从会话表中删除这些会话。比必须查找所有会话来过滤掉特定用户的会话要好得多。
The most efficient way is to store the session id of the user during login. You can access the session ID using request.session._session_key and store it in a separate model which has reference to the user. Now when you want to remove all the sessions of the user, just query this model which will return all the active sessions for the user in question. Now you need to delete only these sessions from the session table. Much better than having to look up all the sessions to filter out just sessions for a particular user.
使用
It might be helpful to use:
这不是直接答案,但它解决了您的问题并将数据库命中率减少到零。使用最新版本的 Django,您可以使用基于 cookie 的会话后端:
https://docs.djangoproject.com/en/dev/topics/http/sessions/#cookie-session-backend
This is not a direct answer, but it solves your problem and reduces DB hits to zero. With recent versions of Django you can use the cookie based session backend:
https://docs.djangoproject.com/en/dev/topics/http/sessions/#cookie-session-backend
我们遇到了类似的情况,我们有一个 SSO 应用程序,它使用不同类型的身份验证/授权解决方案,例如 OAuth、CSR 应用程序的令牌和 SSR 应用程序的 Cookie-Session。在注销期间,我们必须清除所有应用程序中的所有会话和令牌才能实时注销用户。
如果你仔细观察 django 中 Session 模型的源代码,你会发现所有行都有一个 session_key。
主要思想是在登录时找到用户的session_key,然后将其存储在某个地方(最好是用户的模型本身或具有FK的模型),然后在注销期间恢复并删除具有该密钥的会话行。
示例:
此解决方案适用于 django 3,但对于其他版本,会话行为可能有所不同
We were in a similar situation where we had an SSO app that used diffrent kind of authentication/authorization solutions like OAuth, Token for CSR apps, and Cookie-Session for SSR apps. During logout we had to clear all session and tokens from all the apps to logout user realtime.
If you watch closely the source code of Session model in django you will find out all rows have a session_key.
The main idea is to find the user's session_key in login then store it somewhere (it is better to be the user's model itself or a model that has a FK to it), then restore and delete session rows that have this key during logout.
Example:
This solution is for django 3 but for other versions may session behave diffrent