Django 多租户应用跳转子域名后变成 AnonymousUser,如何共享会话?
结论
共享登录状态需要满足两个条件:浏览器向租户子域名发送会话 Cookie,服务端能为租户请求读取同一份会话和用户数据。
你列出的三个设置没有配置 Cookie 的域名范围。如果 SESSION_COOKIE_DOMAIN 仍为默认值 None,登录时设置的 Cookie 通常只发给 localhost,不会发给 tenant1.localhost。建议先检查这里;由于缺少完整配置,暂时不能确定这是唯一原因。Django 配置说明
Tenant 与 CustomUser 的 OneToOne 关系只表达业务关联,不会自动共享登录会话。
1. 配置父域 Cookie,并避开 localhost 的特殊情况
未设置 Domain 的 Cookie 是 host-only Cookie,只发送给设置它的主机。设置父域后,浏览器才能将 Cookie 发送给该父域及其子域。SameSite='Lax' 不会扩大这个范围。Cookie Domain 规则
本地测试时,不宜直接假定 SESSION_COOKIE_DOMAIN = '.localhost' 在目标浏览器中有效。可以改用明确的测试父域,例如 app.test,减少 localhost 特殊处理带来的干扰。
在 hosts 文件中添加:
127.0.0.1 app.test tenant1.app.test
然后配置本地开发环境:
ALLOWED_HOSTS = ['app.test', 'tenant1.app.test']
SESSION_COOKIE_DOMAIN = '.app.test'
SESSION_COOKIE_PATH = '/'
SESSION_COOKIE_SAMESITE = 'Lax'
# Local development over HTTP only
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
改为在 http://app.test:8000 登录,登录后跳转到 http://tenant1.app.test:8000。Cookie 的 Domain 中不要包含协议、端口或路径。
同时更新 django-tenants 的域名记录:主站域名 app.test 对应 public 租户,tenant1.app.test 对应目标租户。域名记录只保存主机名,不包含 http:// 或 :8000。租户域名配置
修改后,清除测试域名下的旧会话 Cookie,再重新登录,避免旧的 host-only Cookie 与父域 Cookie 同名共存。
2. 确认会话和用户数据确实共享
以下建议以数据库会话后端为前提,即使用默认的 django.contrib.sessions.backends.db。
你已经把 account 放入 SHARED_APPS,还需要检查以下应用是否也按共享认证方案配置:
# These entries belong in SHARED_APPS for shared authentication.
'django.contrib.contenttypes'
'django.contrib.auth'
'django.contrib.sessions'
'account'
上面只是需要核对的条目,并非完整的 SHARED_APPS。请保留项目已有的 django_tenants、租户模型所在应用及其他必需应用。对于当前共享登录方案,这些认证相关应用应统一放在共享层,避免在租户 schema 中再次创建独立的用户或会话表。django-tenants 应用配置
正常情况下,租户请求的 PostgreSQL search_path 包含租户 schema 和 public,因此能访问共享表。如果租户 schema 中已经存在同名的 django_session 表,它可能先被查询到,导致主站写入的会话无法读取。需要结合实际表结构核查这种可能性。schema 查询规则
若共享表尚未创建,按项目迁移流程执行:
python manage.py migrate_schemas --shared
如果已有项目曾把应用从 TENANT_APPS 移到 SHARED_APPS,仅修改配置或再次执行迁移未必能修正历史表结构,还需要检查迁移记录和现有数据。
如果使用 Redis、缓存或其他会话后端,应检查两个域名是否访问同一份会话存储,以及缓存键是否按租户隔离;不要直接套用 django_session 表的排查方法。Django 会话后端
3. 检查中间件顺序
TenantMainMiddleware 应位于中间件列表顶部,先根据域名选择 schema;SessionMiddleware 必须先于 AuthenticationMiddleware,让认证过程能够读取会话。
相关条目的顺序应类似:
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
将这一顺序合并到现有配置中,并检查主站与租户请求是否都经过这些中间件。django-tenants 中间件要求、Django 认证中间件要求
4. 根据请求证据定位故障
打开浏览器开发者工具,保留 Network 日志,然后重新登录并完成跳转:
- 查看调用
login(request, user)后返回的响应,包括重定向响应,确认其中有会话 Cookie 的Set-Cookie,并检查浏览器是否提示拒收。 - 查看租户子域名的首次请求,确认请求头
Cookie中包含刚建立的会话 Cookie。默认名称为sessionid;如果修改过SESSION_COOKIE_NAME,以实际名称为准。 - 如果 Cookie 没有发送,继续检查 Domain、Path、Secure 和浏览器拦截原因。
- 如果 Cookie 已发送但仍是
AnonymousUser,转向服务端检查:会话是否可读取、是否过期,认证后端能否按会话中的用户 ID 查到共享的CustomUser,以及 schema 是否选对。
若主站和子域名由不同进程或不同部署提供,还需要核对它们的 SECRET_KEY、认证后端配置和会话存储是否一致。
5. 区分会话共享、CSRF 和租户权限
CSRF_COOKIE_DOMAIN 控制 CSRF Cookie 的作用域,不能代替 SESSION_COOKIE_DOMAIN。如果登录态恢复后出现跨子域 POST 的 CSRF 错误,再根据实际表单来源配置 CSRF;普通页面重定向不需要为了共享会话而关闭 CSRF。
生产环境使用 HTTPS 时,应开启 SESSION_COOKIE_SECURE 和 CSRF_COOKIE_SECURE。父域 Cookie 会被发送给匹配的各个子域,因此服务端仍须检查当前用户是否有权访问 request.tenant。共享登录状态不代表用户拥有所有租户的访问权限。Django 会话安全说明
如果租户使用彼此独立的自定义域名,同一个父域 Cookie 无法覆盖它们,需要通过统一认证入口或一次性登录票据,在各域名分别建立会话。
备注:内容仅供参考。
