Django密码重置邮件链接点击失效、复制粘贴可正常使用问题
问题触发原因
- 核心原因是Gmail、Outlook等邮件服务商的链接安全扫描机制和中转跳转机制共同导致的:
- 主流邮件服务默认会将邮件内的链接替换为自身的中转链接,用户点击时先跳转到邮件服务商域名再跳转回你的站点,属于跨站跳转场景
- 大部分邮件安全网关/中转服务会主动剥离跳转响应中的
Set-Cookie头,防止跨站用户跟踪,导致第一次请求存token时返回的session cookie没有被用户浏览器接收 - 少数情况是站点的
SESSION_COOKIE_SAMESITE配置为Strict,跨站跳转的请求无法携带session cookie,重定向后新的请求对应全新的空session,自然拿不到之前存储的token - 更低概率的场景是邮件爬虫在用户点击前提前访问了带token的链接,将token存入了爬虫的独立会话,该场景下日志会出现两次相同token的dispatch调用,和你提供的日志特征不符,可以优先排查前三种原因
解决方案
方案1(最稳妥,无兼容性问题)
重写PasswordResetConfirmView,取消跳转逻辑,验证token有效后直接渲染密码重置表单,完全规避session存token的依赖:
from django.contrib.auth.views import PasswordResetConfirmView as BasePasswordResetConfirmView from django.contrib.auth import views as auth_views class PasswordResetConfirmView(BasePasswordResetConfirmView): def dispatch(self, *args, **kwargs): self.validlink = False self.user = self.get_user(kwargs['uidb64']) if self.user is not None: token = kwargs['token'] # 带有效token的访问直接设置验证通过,不走跳转逻辑 if token != self.reset_url_token and self.token_generator.check_token(self.user, token): self.validlink = True return super().dispatch(*args, **kwargs) # 保留原set-password路径逻辑,兼容旧访问入口 elif token == self.reset_url_token: session_token = self.request.session.get(auth_views.INTERNAL_RESET_SESSION_TOKEN) if self.token_generator.check_token(self.user, session_token): self.validlink = True return super().dispatch(*args, **kwargs) # 验证失败走原生失败分支 return self.render_to_response(self.get_context_data())
替换urls.py中原本的密码重置确认视图即可,该方案唯一缺点是如果用户从重置页面跳转到第三方站点,token可能会通过Referer头泄露,实际风险极低。
方案2(调整cookie配置,适配跨站跳转)
如果不想修改视图逻辑,可以调整settings.py中的session cookie配置:
# 允许Session Cookie跨站携带,必须同时开启Secure属性 SESSION_COOKIE_SAMESITE = 'None' SESSION_COOKIE_SECURE = True # 若使用CSRF校验,同步调整CSRF Cookie配置 CSRF_COOKIE_SAMESITE = 'None' CSRF_COOKIE_SECURE = True
注意该方案仅支持HTTPS站点,且对2020年之前发布的旧版本浏览器存在兼容性问题。
方案3(增加中间跳转页)
邮件中发送的链接改为无敏感参数的中间页地址,页面上放置「重置密码」按钮,用户点击按钮后再跳转到带token的重置地址,完全规避邮件爬虫和中转跳转的影响,同时可以保留Django原生的所有安全逻辑。
内容的提问来源于stack exchange,提问作者run_the_race
相关产品推荐
相关产品推荐

