에러

KeyError at /user/password_reset_confirm/MjQ/set-password/ 에러 해결 기록

integerJI 2020. 8. 27. 22:27

PasswordResetConfirmView를 상속하며 문제를 해결해 나아가는 과정 기록

 

원인은 맨 밑으로 또한 결과를 보시려면

 

url

 

기존의 코드 소개

views.py

from django.contrib.auth.views import LoginView, LogoutView, PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView
from django.contrib.auth.tokens import default_token_generator
from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url, urlsafe_base64_decode
from django.contrib.auth import (
    REDIRECT_FIELD_NAME, get_user_model, login as auth_login,
    logout as auth_logout, update_session_auth_hash,
)
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
from django.core.exceptions import ValidationError

# 해당 소스는 class 밖에 들어갑니다.
INTERNAL_RESET_URL_TOKEN = 'set-password'
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'

class UserPasswordResetConfirmView(PasswordResetConfirmView):
    template_name = 'password_reset_confirm.html'
    token_generator = default_token_generator
	success_url = reverse_lazy('password_reset_complete')

    @method_decorator(sensitive_post_parameters())
    @method_decorator(never_cache)
    def dispatch(self, *args, **kwargs):
        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']
            if token == INTERNAL_RESET_URL_TOKEN:
                session_token = self.request.session.get(INTERNAL_RESET_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:
                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
                    redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN)
                    return HttpResponseRedirect(redirect_url)

        # Display the "Password reset unsuccessful" page.
        return self.render_to_response(self.get_context_data())

    def get_user(self, uidb64):
        try:
            # urlsafe_base64_decode() decodes to bytestring
            uid = urlsafe_base64_decode(uidb64).decode()
            user = User._default_manager.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
            user = None
        return user

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['user'] = self.user
        return kwargs

    def form_valid(self, form):
        user = form.save()
        del self.request.session[INTERNAL_RESET_SESSION_TOKEN]
        if self.post_reset_login:
            auth_login(self.request, user, self.post_reset_login_backend)
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.validlink:
            context['validlink'] = True
        else:
            context.update({
                'form': None,
                'title': _('Password reset unsuccessful'),
                'validlink': False,
            })
        return context

 

password_reset_confirm.html

<h1>비번 바꾸는 창</h1>
<form method="post" action="{% url 'password_reset' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">회원가입</button>
</form>

 

PasswordResetView를 그대로 가져왔지만 비밀번호를 바꾸며 문제가 생겼다.

 

https://github.com/django/django/pull/8062

https://code.djangoproject.com/ticket/27840

https://code.djangoproject.com/ticket/30952

 

다른 글들을 보니 나와 같은 사람이 많았다.

 

글을 읽어보니 로그인한 사용자의 세션이 물려있어서 그렇다는데..

 

del self.request.session[INTERNAL_RESET_SESSION_TOKEN]

 

해당 부분을 pop으로도 바꿔보고

 

위로 올려서 먼저 del이 실행되게도 해보았다.

 

2020-08-25 23:16

print 시도

너무 모르겠다.

 

솔직히 너무 모르겠어서 함수마다 모두 print를 뽑아 어디서 에러가 나오는지 확인해 보았다.

 

form_valid까지 찍히고 이하부터 에러가 찍혔다

 

Internal Server Error: /user/password_reset_confirm/MjQ/set-password/

 

uidb64에 문제가 있을 수도 있다. urls.py의 문제일 수도?

 

 

2020-08-25 23:25

개발자 도구를 이용한 parameter 확인 시도

 

django에서 제공하는 auth_views.PasswordResetConfirmView.as_view()

 

새로 만든 views.UserPasswordResetConfirmView.as_view()

 

기존의 url과 새로 만든 url의 값을 비교합니다.

 

차이점 x

 

흠..

 

 

2020-08-25 23:50

https://www.it-swarm.dev/ko/python/django-20-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%9D%B8%EC%88%98-uidb64%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-noreversematch/835429132/

 

Django 2.2+, urlsafe_base64_encode문자열 반환에서, 디코딩할 필요가 없습니다.라는 글을 발견

 

 

uid에서. decode 변환 부분을 삭제

 

 

실패

 

문제를 찾으면서의 키워드

 

 - Internal Server Error: /user/password_reset_confirm/MjQ/set-password/

 - django uid

 

2020-08-26 22:30

문득 생각났다.

 

django 백 단 문제가 아니라면?

 

path('password_reset_confirm/<uidb64>/<token>/', views.UserPasswordResetConfirmView.as_view(), name="password_reset_confirm"),

 

urls.py에 보면 uidb64와 함께 토큰 값을 같이 넘겨주고 있다.

 

하지만 html을 봐보면?

 

 

그냥 submit를 누르면 form이 실행이 되게 되어있다.

 

그래서 넣어 보았다.

{% extends 'signBase.html' %}
{% load static %}
{% block signcontent %}

<body class="text-center">

<form class="form-signin" method="POST" action="{% url 'password_reset_confirm' uidb64=uid token=token %}">

    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">비밀번호 초기화</button>
</form>

</body>

{% endblock %}

form url에 uid와 토큰을 넣어주었다.

 

 

물론 실패하였지만 뭔가 느낌이 있다. 해결할 거 같은 느낌이

 

2020-08-26 22:43

혹시 몰라 auth에 있는 url과 비교해봤다

 

달랐던 점을 수정해 보았다

 

실패

 

2020-08-26 23:20

https://github.com/awesto/django-shop/issues/752

 

Password reset confirm view · Issue #752 · awesto/django-shop

Hi I have an issue, which occured while testing password reset. I am using the i18n demo. The first part is working fine: I can click the link, send a amail to my address and klick the link. Then I...

github.com

좀 비슷한 거 같다. 특히 마지막 말

 

We found a temporary workaround for this issue:
I have overwritten def post(self, request, uidb64=None, token=None) of PasswordResetConfirm and I validate the request myself.
The issue has been, despite being a valid form and link, validation in PasswordResetConfirmSerializer has not been working.
I still do not understand, why def get is validating the link properly, and def post isn't, but resetting the password is working now.

메모..

 

2020-08-27 21:43

UserPasswordResetConfirmView 함수 수정

 

 

수정된 부분은 views.py에서 모두 지우고 form 만 넘기는 부분이었다.

 

왜냐하면 print로 로그를 찍었을 때 form_valid 다음에서 막혔기 때문이다..

 

 

html도 다시 submit를 누르면 form이 post로 실행되게 바꾸었었다..

 

<body class="text-center">

    <form action="" method="post">

    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">비밀번호 초기화</button>
</form>

</body>

 

2020-08-27 21:43

실행이 되었다.

 

아무래도 클래스를 상속받으면서 같은 소스가 중복되어 호출되어 값이 꼬인 거 같다.

 

아무 생각 없이 소스를 상속받아야 한다 라는 생각이 머릿속에 자리 잡았던 것 같다.

 

소스를 상속받을 때 해당 소스가 무슨 기능을 하는지 어떤 순서로 실행이 되는지

 

또한 나의 소스와 어떠한 연결점을 가지고 실행이 되는지 생각할 수 있는 좋은 기회였다.