URL-адрес для сброса пароля Django в электронной почте работает только в том случае, если его скопировать и вставить, а не щелкнуть по нему

#django

Вопрос:

Когда я запрошу сброс пароля с помощью встроенного механизма django, он сгенерирует URL-адрес в электронном письме следующим образом:

 https://example/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/
 

В gmail, если я покажу URL-адрес, текст будет неопределенным, так как он создается как обычное/текстовое электронное письмо. Если я нажму ссылку в gmail или Outlook, то она отправится (возможно, после перенаправления, я не могу сказать, так как это происходит так быстро):

 https://example.com/accounts/reset/MTA/set-password/
 

И там написано

 Password reset unsuccessful
The password reset link was invalid, possibly because it has already been used. Please request a new password reset.
 

Однако если я скопирую и вставлю URL-адрес в браузер, он будет работать. Однако никто не копирует и не вставляет URL-адрес, все они нажимают на ссылку и говорят, что она не работает.
Я не понимаю, почему нажатие на ссылку не работает, так как она правильная?

Мне удалось добавить ведение журнала в код Django, я добавил ведение журнала в:

 contrib.auth.views -> PasswordResetConfirmView.dispatch (line 247)
 

Как вы можете видеть из следующих двух журналов, в обоих сценариях они вызываются с точно такими же локальными объектами (я печатаю локальные объекты()), однако это представление, похоже, устанавливает токен в сеансе, а затем перенаправляет. однако после перенаправления, если кто-то щелкнул ссылку в электронном письме, файл cookie сеанса исчез, и, следовательно, он не работает. Я все еще не понимаю, почему

СКОПИРУЙТЕ и вставьте URL-адрес (рабочий)

 ------------------------------------------------------------
# First call to dispatch:
token: atspc3-7c45df8a600243fde3dfb60c44873f15
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20e63c2220>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15'}, 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is NOT reset
token atspc3-7c45df8a600243fde3dfb60c44873f15
Verfiy sessions token atszyc-e7c578496e3438f9dee367fcbebfabb1
redirect url /accounts/reset/MTA/set-password/
Djnago logging start
------------------------------------------------------------
# Second call to dispatch (after redirect):
token: set-password
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20e63c2220>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/set-password/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'set-password'}, 'token': 'set-password', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is self reset, session token: atspc3-7c45df8a600243fde3dfb60c44873f15
 

НАЖМИТЕ ссылку в электронном письме (не работает)

 ------------------------------------------------------------
# First call to dispatch:
token: atspc3-7c45df8a600243fde3dfb60c44873f15
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20d9a77190>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15'}, 'token': 'atspc3-7c45df8a600243fde3dfb60c44873f15', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is NOT reset
token atspc3-7c45df8a600243fde3dfb60c44873f15
Verfiy sessions token atszyc-e7c578496e3438f9dee367fcbebfabb1
redirect url /accounts/reset/MTA/set-password/
------------------------------------------------------------
# Second call to dispatch (after redirect):
token: set-password
Entered dispatch {'self': <django.contrib.auth.views.PasswordResetConfirmView object at 0x7f20d9adde20>, 'args': (<WSGIRequest: GET '/accounts/reset/MTA/set-password/'>,), 'kwargs': {'uidb64': 'MTA', 'token': 'set-password'}, 'token': 'set-password', '__class__': <class 'django.contrib.auth.views.PasswordResetConfirmView'>}
user is not None
token is self reset, session token: None
 

Вот инструкции журнала, которые я добавил в contrib.auth.views -> PasswordResetConfirmView.dispatch (line 247) :

 @method_decorator(never_cache)
    def dispatch(self, *args, **kwargs):
        log_info('------------------------------------------------------------')
        token = kwargs['token']
        log_info('token:', token)
        log_info('Entered dispatch', str(locals()))
        assert 'uidb64' in kwargs and 'token' in kwargs

        self.validlink = False
        self.user = self.get_user(kwargs['uidb64'])

        if self.user is not None:
            token = kwargs['token']
            log_info('user is not None')
            if token == self.reset_url_token:
                session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
                log_info('token is self reset, session token:', session_token)
                if self.token_generator.check_token(self.user, session_token):
                    # If the token is valid, display the password reset form.
                    self.validlink = True
                    return super().dispatch(*args, **kwargs)
            else:
                log_info('token is NOT reset')
                if self.token_generator.check_token(self.user, token):
                    # Store the token in the session and redirect to the
                    # password reset form at a URL without the token. That
                    # avoids the possibility of leaking the token in the
                    # HTTP Referer header.
                    self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
                    log_info('token', token)
                    log_info('Verfiy sessions token', self.request.session[INTERNAL_RESET_SESSION_TOKEN])
                    redirect_url = self.request.path.replace(token, self.reset_url_token)
                    log_info('redirect url', redirect_url)
                    return HttpResponseRedirect(redirect_url)
                else:
                    log_info('failed token generator check')
 

Обратите внимание, что в обоих случаях запрос имеет следующие атрибуты (я подумал, что это может быть проблема с защищенными файлами cookie).:

 request.scheme: https
request.is_secure: True
request.full path /accounts/reset/MTA/set-password/
or
request.full path /accounts/reset/MTA/atspc3-7c45df8a600243fde3dfb60c44873f15/
 

Комментарии:

1. Не могли бы вы поделиться своим шаблоном электронной почты, urls.py а виды?

2. @black, я использую встроенный шаблон электронной почты, URL-адреса и представления. Я ничего не настраивал. Это сбивает с толку, я не знаю, что не работает. ссылка работает, ссылка в электронном письме та же, нажатие на ссылку в электронном письме не работает. Я бы исправил URL-адрес, но URL-адрес уже правильный.

3. Ситуация определенно неожиданная, поэтому нам нужно просмотреть ваш код, чтобы понять, в чем проблема.

4. @black, спасибо за продолжение, к сожалению, это не мой код, но, пожалуйста, посмотрите некоторые записи, которые я добавил в код django.

Ответ №1:

В редких случаях, когда у кого-то еще есть эта проблема, у меня была в файле настроек:

 SESSION_COOKIE_SAMESITE = 'Strict'
 

Изменение его на разрешенный файл cookie для сохранения из внешнего домена (почтовый клиент):

 SESSION_COOKIE_SAMESITE = 'Lax'  # default value
 

Обратитесь к https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-samesite