앞서 회원가입과 로그인에 대해 살펴보았기 때문에, 다음으로 확인해볼 기능은 사용자 계정의 제어, 그 중에서도 비밀번호의 초기화입니다. dj-rest-auth는 비밀번호의 변경과 초기화 모두 지원하지만, 변경 기능의 경우 크게 복잡할 것이 없기때문에, 초기화 기능만 살펴보려고 합니다.
존재하는 사용자의 비밀번호를 초기화하는 기능은 django, django-allauth 둘 모두에 의존하고 있으며, django가 제공하는(그러나 보통 잘 사용하지 않는) frontend 작업을 위한 Form과도 일부 연관이 있습니다. 이 글에서는 frontend의 상세한 제어부분은 생략하도록 하고, 다음의 클래스들만을 다룰 예정입니다.
PasswordResetView
PasswordResetSerializer
AllAuthPassswordResetForm / PasswordResetForm
기능의 흐름은 대략 다음과 같습니다.
그렇기 때문에, 기본적으로 이 기능을 사용하기 위해서는 settings에 smtp 프로토콜을 사용하기 위한 설정들이 모두 되어 있어야 하고, 사용자 계정에 이메일 필드가 반드시 정의되어야합니다. 만약 이메일을 사용하지 않으려고 한다면 커스텀이 필요하고요. 이 글에서는 이메일이 존재한다는 가정하에 서술하되, 이메일 외의 필드로 사용하는 방법은 간략히 언급하도록 하겠습니다.
Post 메서드를 상정하고 있으나, view에는 기능이 거의 없습니다.
class PasswordResetView(GenericAPIView):
serializer_class = api_settings.PASSWORD_RESET_SERIALIZER
permission_classes = (AllowAny,)
throttle_scope = 'dj_rest_auth'
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{'detail': _('Password reset e-mail has been sent.')},
status=status.HTTP_200_OK,
)
요청의 입력값에 대해, 설정에 세팅되어 있는 serializer를 호출하여 validation과 save메서드를 호출 후, 응답을 반환하는 것이 전부입니다. Serializer를 확인해보면
class PasswordResetSerializer(serializers.Serializer):
email = serializers.EmailField()
reset_form = None
@property
def password_reset_form_class(self):
if 'allauth' in settings.INSTALLED_APPS:
return AllAuthPasswordResetForm
else:
return PasswordResetForm
def get_email_options(self):
return {}
def validate_email(self, value):
self.reset_form = self.password_reset_form_class(data=self.initial_data)
if not self.reset_form.is_valid():
raise serializers.ValidationError(self.reset_form.errors)
return value
def save(self):
if 'allauth' in settings.INSTALLED_APPS:
from allauth.account.forms import default_token_generator
else:
from django.contrib.auth.tokens import default_token_generator
request = self.context.get('request')
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
'token_generator': default_token_generator,
}
opts.update(self.get_email_options())
self.reset_form.save(**opts)
이메일을 기본 입력값으로 받고, 적절한 password reset form을 호출한 뒤, 해당 form의 save 메서드에 opts를 인자로 주고 실행합니다.
이 모든 호출의 결과로, 입력 email주소로 초기화에 필요한 url을 작성해서 보냅니다. 다음으로는 form 클래스들을 살펴보겠습니다.
전자는 django에 작성되어 있으며, 후자는 django-allauth에 작성되어 있습니다.
PasswordResetForm은 email 변수와 get_user, save, send_mail 메서드가 작성되어 있습니다.
def save(
self,
domain_override=None,
subject_template_name="registration/password_reset_subject.txt",
email_template_name="registration/password_reset_email.html",
use_https=False,
token_generator=default_token_generator,
from_email=None,
request=None,
html_email_template_name=None,
extra_email_context=None,
):
email = self.cleaned_data["email"]
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override
email_field_name = UserModel.get_email_field_name()
for user in self.get_users(email):
user_email = getattr(user, email_field_name)
context = {
"email": user_email,
"domain": domain,
"site_name": site_name,
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"user": user,
"token": token_generator.make_token(user),
"protocol": "https" if use_https else "http",
**(extra_email_context or {}),
}
self.send_mail(
subject_template_name,
email_template_name,
context,
from_email,
user_email,
html_email_template_name=html_email_template_name,
)
앞서 serializer에서 전달한 opts 중에는 use_https, from_email, request, token_generator가 있었습니다. 위의 save메서드는 각각을 기반으로 프로토콜, 발신자 이메일 주소, 도메인, 토큰을 정합니다.
그리고 serializer에서 form을 생성하면서 정해졌던 email을 기반으로, get_user 메서드를 호출하여 해당 email로 가져올 수 있는 모든 user를 가져옵니다. 그 후, 개별 user별로 context를 작성하여 send_mail메서드를 호출하는 것을 통하여, 이메일을 보냅니다.
즉, email을 이용하지 않으려고 하면서 django의 form을 기반으로 기능을 사용하고자 한다면, serializer, view, form의 이메일 필드를 사용하고자하려는 필드로 바꾸고 validation들을 교체해준 다음에, 최종적으로 form의 get user 메서드를 수정하여 새 필드로 user를 필터링하여 반환토록하면 됩니다.
다음으로는 AllAuthPasswordResetForm을 살펴보겠습니다. 이 클래스에는 마찬가지로 이메일 필드가 선언되어 있고, save, clean_email, _send_password_reset_mail이 매서드로 제공되어 있습니다.
def clean_email(self):
email = self.cleaned_data["email"].lower()
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email, is_active=True, prefer_verified=True)
if not self.users and not app_settings.PREVENT_ENUMERATION:
raise get_adapter().validation_error("unknown_email")
return self.cleaned_data["email"]
def save(self, request, **kwargs):
email = self.cleaned_data["email"]
if not self.users:
if app_settings.EMAIL_UNKNOWN_ACCOUNTS:
flows.signup.send_unknown_account_mail(request, email)
else:
self._send_password_reset_mail(request, email, self.users, **kwargs)
return email
def _send_password_reset_mail(self, request, email, users, **kwargs):
token_generator = kwargs.get("token_generator", default_token_generator)
for user in users:
temp_key = token_generator.make_token(user)
# send the password reset email
uid = user_pk_to_url_str(user)
key = f"{uid}-{temp_key}"
url = get_adapter().get_reset_password_from_key_url(key)
context = {
"user": user,
"password_reset_url": url,
"uid": uid,
"key": temp_key,
"request": request,
}
if app_settings.AUTHENTICATION_METHOD != AuthenticationMethod.EMAIL:
context["username"] = user_username(user)
get_adapter().send_mail("account/email/password_reset_key", email, context)
이 클래스에서는 clean_email 메서드를 통해 사용자를 필터링하고, 전달받은 opts를 전부 kwarg로 넘겨서 메일을 작성하여 보냅니다. 이때 메일 전송을 form에서 처리하지 않고, 앞서 간략히 소개한 DefaultAdapter의 메서드를 사용하여 처리하고 있습니다.
그래서 AllAuth기반으로 이메일 말고 다른 입력값으로 변경하고 싶다면, adapter까지 필요한 부분을 커스텀하여 적용해줄 필요가 있습니다.