如何“Refresh” Django session variables 避免超时?
结论
不需要为了续期,先读取 session 变量再把它赋回去。这种写法可能会触发 session 保存,但表达不出真实意图,还会带来不必要的写操作。
可以按实际需求选择:
- 如果希望用户持续操作时不会超时,使用
SESSION_SAVE_EVERY_REQUEST = True实现滑动过期。 - 如果只需要在注册流程中续期,显式设置
request.session.modified = True。 - 使用
request.session.set_expiry()为注册 session 设置合适的有效期。 - 将注册进度和支付状态持久化到数据库,不要只保存在 session 中。支付回调尤其不能依赖浏览器 session。
使用滑动过期
Django 默认只在 session 内容发生变化时保存 session。下面的代码只读取数据,不会刷新过期时间:
user_id = request.session.get("signup_user_id")
可以通过以下配置启用滑动过期:
# settings.py
SESSION_COOKIE_AGE = 60 * 60 # 1 小时
SESSION_SAVE_EVERY_REQUEST = True
配置后,Django 会在每次请求结束时保存 session,并重新发送 session cookie。只要用户持续访问网站,过期时间就会随请求向后延长。
这种方式适合实现“连续 30 分钟或 1 小时没有操作后超时”,但会作用于整个站点,也会增加 session 存储的写入次数。如果使用数据库、缓存或 cached_db session backend,需要评估这些额外写入的影响。
只刷新注册流程中的 session
如果不想在全站启用 SESSION_SAVE_EVERY_REQUEST,可以只在注册流程的视图中明确标记 session 已修改:
from django.shortcuts import redirect, render
def signup_profile(request):
user_id = request.session.get("signup_user_id")
if user_id is None:
return redirect("signup_start")
request.session.modified = True
return render(request, "signup/profile.html", {
"user_id": user_id,
})
这种写法比读取后再赋回原值更能准确表达续期意图:
# 不推荐仅为了续期而这样写
request.session["signup_user_id"] = request.session["signup_user_id"]
如果多个注册页面都要做相同处理,可以封装成装饰器或中间件:
from functools import wraps
def keep_signup_session_alive(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
if request.session.get("signup_user_id") is not None:
request.session.modified = True
return response
return wrapper
使用方式如下:
@keep_signup_session_alive
def signup_review(request):
return render(request, "signup/review.html")
GET 请求原则上不应修改业务数据,不过更新 session 的访问时间属于会话管理操作,通常可以接受。仍要避免静态资源请求、轮询请求或机器人访问无意义地延长 session。
为注册流程设置单独的过期时间
创建注册 session 时,可以调用 set_expiry():
def signup_start(request):
# 创建账户后获得 user.id
user = create_pending_user(request)
request.session["signup_user_id"] = user.id
request.session["signup_username"] = user.username
request.session.set_expiry(60 * 60) # 1 小时
return redirect("signup_profile")
set_expiry(3600) 表示该 session 的过期周期为 3600 秒。如果希望每次注册请求都能续期,还需要在这些请求中保存 session,例如设置:
request.session.modified = True
也可以在全局启用:
SESSION_SAVE_EVERY_REQUEST = True
set_expiry(3600) 并不意味着注册流程一定能持续一小时。浏览器关闭、cookie 被清除、session 存储失效或 session key 被轮换,都可能导致原 session 无法继续使用。
注册进度不应只保存在 session
session 适合保存当前注册流程的引用,例如:
request.session["signup_id"] = str(signup.id)
profile、照片、当前步骤和支付状态等重要数据,更适合保存在数据库中:
class SignupProcess(models.Model):
user = models.OneToOneField(
"auth.User",
on_delete=models.CASCADE,
)
current_step = models.PositiveSmallIntegerField(default=1)
payment_status = models.CharField(
max_length=20,
default="pending",
)
completed = models.BooleanField(default=False)
updated_at = models.DateTimeField(auto_now=True)
这样即使 session 超时,也能在验证用户身份后恢复注册流程,不必让用户重新开始。
数据库中的待注册记录还可以设置业务有效期,例如允许用户在创建记录后的 24 小时内继续注册。session 则可以保持较短的安全超时时间。这两个有效期分别处理不同的问题:
- session 有效期控制浏览器会话。
- 注册记录有效期控制未完成流程的保留时间。
支付回调不要依赖用户 session
支付处理器发送回调时,通常不会携带用户浏览器中的 Django session cookie。因此,不能通过下面的值识别支付对应的用户:
request.session["signup_user_id"]
发起支付时,应先创建支付记录,再向支付处理器传递订单号或不可猜测的业务引用:
payment = Payment.objects.create(
user=user,
status="pending",
)
payload = {
"merchant_reference": str(payment.public_id),
"amount": payment.amount,
}
收到支付回调后,需要:
- 验证支付平台的签名或 webhook 身份。
- 根据商户订单号查找本地支付记录。
- 核对金额、币种和支付状态。
- 以幂等方式更新支付结果。
- 将注册流程标记为已支付或已完成。
用户返回网站后,再根据数据库中的支付状态判断是否完成注册。不要仅凭浏览器传回的 Payment OK 参数登录用户,因为这个参数可能被伪造。
登录成功后的处理
确认支付成功后,可以使用 Django 的 login():
from django.contrib.auth import login
from django.shortcuts import redirect
def payment_return(request):
signup = get_verified_signup(request)
if not signup.payment_is_confirmed:
return redirect("payment_pending")
login(request, signup.user)
signup.completed = True
signup.save(update_fields=["completed"])
for key in (
"signup_id",
"signup_user_id",
"signup_username",
):
request.session.pop(key, None)
return redirect("home")
login() 可能会轮换 session key,以降低 session fixation 风险。因此,注册代码不应假设登录前后的 session key 始终相同。
推荐方案
这个流程可以采用以下配置:
# settings.py
SESSION_COOKIE_AGE = 60 * 60
SESSION_SAVE_EVERY_REQUEST = False
注册开始时设置过期时间:
request.session.set_expiry(60 * 60)
在注册流程的有效请求中显式续期:
request.session.modified = True
注册进度、上传结果和支付状态则保存到数据库。这样可以减少全站的 session 写入,同时让仍在操作的用户顺利完成注册。即使 session 意外失效,关键业务状态也不会丢失。
备注:内容仅供参考。
