PAYATHON 2026

如何“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,
}

收到支付回调后,需要:

  1. 验证支付平台的签名或 webhook 身份。
  2. 根据商户订单号查找本地支付记录。
  3. 核对金额、币种和支付状态。
  4. 以幂等方式更新支付结果。
  5. 将注册流程标记为已支付或已完成。

用户返回网站后,再根据数据库中的支付状态判断是否完成注册。不要仅凭浏览器传回的 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 意外失效,关键业务状态也不会丢失。

备注:内容仅供参考。