[컴][웹] django registration 을 사용해서 email 확인 절차 추가

장고에서 유저 등록시 이메일 확인절차 / email verification for django




ref. 1 에서 제공하는 library 는 아래 2가지 case 를 위해 사용할 수 있다.
  1. 2-phase 등록: 가입하고, email 확인 하는 절차후에 완료
  2. 1-phase 등록: 가입하는 즉시, login 되고 완료


django-registration 설치

pip install django-registration



설정 : 2-phase 등록, HMAC activation workflow


동작원리



INSTALLED_APPS, migrate

django-registration 이 djagno.contrib.auth 를 사용하기 때문에, setting 의 INSTALLED_APPS 에 django.contrib.auth 가 있어야 하고(보통 default 로 되어 있다.), manage.py migrate 을 통해 db 를 생성해 놔야 한다.




URL 등록

from django.conf.urls import include, url

urlpatterns = [
    # Other URL patterns ...
    url(r'^accounts/', include('registration.backends.hmac.urls')),
    # More URL patterns ...
]
  • accounts/register : form 에서 submit 을 눌러서 user 를 등록할 때 사용. form 의 action 에 쓰이는 url 이라고 보면 된다.
  • accounts/login: user 가 activated 되고 나서 login 할 때 사용

template

django-registration 에서 사용하는 template 을 만든다.

에 보면, 필요한 template 들을 확인할 수 있다.
template folder 안에 registration folder 를 하나 만들어서 넣으면 된다. 아래 url 에서 예제를 볼 수 있다.

노란색 부분은 단순한 template view 로 되어 있다.(hmac/urls.py 참고) 그래서 html 이외의 사항을 수정하고 싶다면, 이 view 를 수정하는 것이 아니라, get_success_url() 부분에서 다른 url 을 호출하는 방식을 써야 한다.
  1. registration/registration_form.html : {{form}}
  2. registration/registration_complete.html
  3. registration/activate.html: {{activation_key}}
  4. registration/activation_complete.html
  5. registration/activation_email_subject.txt: {{activation_key}} / {{expiration_days}} / {{site}}
  6. registration/activation_email.txt: {{activation_key}} / {{expiration_days}} / {{site}}
  7. registration/registration_closed.html



관련 settings

  • ACCOUNT_ACTIVATION_DAYS : activation 유효 시간, 7은 7일을 뜻한다.
  • REGISTRATION_OPEN : 새로운 account 의 등록이 가능한지 여부. default 는 True 이다.
  • REGISTRATION_SALT : HMAC 함수처리할 때 쓰이는 salt string 을 설정한다.


# settings.py
# #### django-registraiton CONFIGURATION
ACCOUNT_ACTIVATION_DAYS=7
REGISTRATION_OPEN=True
REGISTRATION_SALT='mysalt'
# #### END django-registraiton CONFIGURATION


dummy smtp server

아래처럼 command 를 입력하면 smtp 서버를 사용할 수 있다. 다만 auth 와 TLS 등은 지원하지 않으니, 끄고 사용하면 된다. 
실제로 email 를 배달해 주지는 않고 smtp 로 보내는 내용을 화면에 뿌려준다.
C:\>c:\Python27\python.exe -m smtpd -n -c DebuggingServer localhost:465

smtp.gmail.com 설정






실제 적용


get_url_success 관련

django-registration 에서 register 나 activate 를 하고 난 후에 특정 url 로 redirect 시키게 되는데, 그 url 을 받아오는 부분이 get_url_success 이다. 그래서 register 와 activate 이외의 view 들은 view 를 수정하기 보다는 get_url_success 로 다른 view 를 호출하도록 하면 된다.

물론 template 만 수정하는 경우라면 굳이 그러지 않아도 된다.


account/register, ajax 사용

처음에 form 을 하나 가진 view 를 만들자. 여기서는 registration_form.html 을 사용하지 않는 방법을 사용한다. 기존의 html 이 있어서 그것에 맞춰서 작업할 것이다.

필자가 가지고 있는 기존의 view(acc/login 이라하자.) 는 url 을 호출하면 id/pw 입력창과 submit 버튼이 있는 page 이다. 여기서 submit 버튼을 누르면 ajax 로 email 과 password 가 acc/register url 로 전달이 되고, 이것을 UserRegistrationForm 의 형태로 받아서 작업을 했다.

django-registration 을 적용하면, 기존의 view(acc/login) 에서 submit 을 하면 위에서 이야기한 django-registration 의 "account/register/" 를 호출하게 만들어야 된다.

아래 예제에서는 이 "account/register" 가 "acc/register-email" 로 변경되었다. 왜냐하면, django-registration 에서 제공하는 RegistrationView 를 extends 해서 수정해야 했기 때문이다.

그리고 ajax 로 호출을 하다 보니 redirect 는 동작하지 않는다. 그래서 account/complete 를 사용할 수 없었다. 그래서 form_valid() 를 수정해서, JsonResponse() 를 return 하게 수정하였다.


간단하게 흐름을 적어보면 아래와 같다.
  • /register-user/ : email 과 password 입력하는 창이 있는 page를 보여준다.
  • --> submit button 클릭 
  • --> /acc/register-email 
  • --> user 등록 성공(서버)
  • --> email 전송 (서버)
  • --> success/error JsonResponse 전송(서버)
이렇게 하면, 일단 verification email(확인 메일) 까지 전송된 것이다.


여기서, 실제 자신이 직접 작업해야 하는 code 는 아래 2개 이다.
  • class RegistrationView2
  • class UserRegistrationForm

code

# - url(r'^acc/', include('accblacksmith.urls', namespace='acc')),
urlpatterns = [
    ...
    url(r'^hmac/', include('registration.backends.hmac.urls', namespace='hmac')),
    url(r'^register-email/$', RegistrationView2.as_view(), name='register_email'),
    url(r'^activate/(?P[-:\w]+)/$', ActivationView.as_view(), name='register_activate'),
]


# client javascript
@$regForm.find('button').on 'click', (e)->
    url = '/acc/register-email/'
    param = that.$regForm.serialize()

    mine.utils.ajax   # 여기서 csrf 를 포함시킨다.
        url: url,
        type: 'POST'
        data: param
        success: (res) ->
            console.log(res)

        error: (xhr, status, err) ->
            console.log('error occurred')
            return  

    return

# server
from registration.backends.hmac.views import RegistrationView as HmacRegView

class RegistrationView2(HmacRegView):
    """
    This uses the django-registration module

    """
    form_class = UserRegistrationForm

    def get_success_url(self, user):
        return ('acc:hmac:registration_complete', (), {})

    # 원래 여기서 get_success_url 을 사용하지만, json response 를 사용하다 보니
    # get_success_url 은 사용할 필요가 없어졌다.
    def form_valid(self, form):
        try:
            new_user = self.register(form)
        except Exception as e:
            return JsonResponse(err({'message':e.message}), status=400)


        # success_url may be a simple string, or a tuple providing the
        # full argument set for redirect(). Attempting to unpack it
        # tells us which one it is.
        return JsonResponse(success({
            'message' : "registration has been successfully."
        }))

    def create_inactive_user(self, form):
        """
        Invoked by : self.register()
        
        Create the inactive user account and send an email containing
        activation instructions.

        
        """
        
        # create User database record
        #  @see self.form_class.saveWithActive
        new_user = form.saveWithActive(commit=True, active=False)

        # set AccountInfo
        # To add additional information from registration form, you MUST add a code here.
        accinfo = AccountInfo()
        accinfo.user = new_user
        accinfo.phone_no = form.data['phone']
        accinfo.save()

        self.send_activation_email(new_user)

        return new_user

    def send_activation_email(self, user):
        """
        Send the activation email. The activation key is simply the
        username, signed using TimestampSigner.

        """
        activation_key = self.get_activation_key(user)
        context = self.get_email_context(activation_key)
        context.update({
            # add server port info
            'server_port': self.request.META['SERVER_PORT']
        })

        subject = render_to_string(self.email_subject_template,
                                   context)
        # Force subject to a single line to avoid header-injection
        # issues.
        subject = ''.join(subject.splitlines())
        message = render_to_string(self.email_body_template,
                                   context)

        kwargs = {
            "subject": subject,
            "body": message,
            "from_email": settings.EMAIL_HOST_USER,
            "to": [user.email],
            "reply_to": [settings.DEFAULT_FROM_EMAIL],
        }
        email = EmailMultiAlternatives(**kwargs)
        email.send()

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class UserRegistrationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('email', 'password1', 'password2')

    def save(self, commit=True):
        return self.saveWithActive(commit=True, active=True)

    def saveWithActive(self, commit=True, active=True):
        user = super(UserRegistrationForm, self).save(commit=False)
        user.username = user.email
        user.is_active = active  # to verify the email before make it active

        if commit:
            user.save()

        return user

# registration.views.py
class RegistrationView(FormView):
    ...

    def form_valid(self, form):
        new_user = self.register(form)
        success_url = self.get_success_url(new_user)

        # success_url may be a simple string, or a tuple providing the
        # full argument set for redirect(). Attempting to unpack it
        # tells us which one it is.
        try:
            to, args, kwargs = success_url
            return redirect(to, *args, **kwargs)
        except ValueError:
            return redirect(success_url)

class FormView(TemplateResponseMixin, BaseFormView):
    ...

class BaseFormView(FormMixin, ProcessFormView):
    ...

class ProcessFormView(View):
    def get(self, request, *args, **kwargs):
        return self.render_to_response(self.get_context_data())

    def post(self, request, *args, **kwargs):
        
        form = self.get_form()
        if form.is_valid():     
            return self.form_valid(form)    # RegistrationView.form_valid()
        else:
            return self.form_invalid(form)
    ...
# django.views.generic.edit.py
class FormMixin(six.with_metaclass(FormMixinBase, ContextMixin)):
    ...
    def get_form_class(self):
        return self.form_class

    def get_form(self, form_class=None):
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(**self.get_form_kwargs())     # form_class : UserRegistrationForm



account/activate

activation_email.txt 설정

account/register 에서 validation mail 을 보낼때 activation_email.txt 의 내용이 body 로 가게 되는데, 이 때 아래처럼 activation code 를 만들 수 있다.
To activate your account, please visit the following page:

{% if request.is_secure %}https{% else %}http{% endif %}://{{ site.domain }}:9080{% url 'acc:register_activate' activation_key %}

This page will expire in {{ expiration_days }} day{{ expiration_days|pluralize }}.

그럼 아래같은 값이 될 것이다.
  • http://localhost:9080/acc/activate/3rcbVHhchIEho3fEgl28-/


get_success_url() 수정

그런데 여기서는 registration_activation_complete 의 namespace 가 달라서 get_success_url() 을 수정해야 했다. 그래서 ActivationView 를 상속받아서 새로운 ActivationView 를 만들었다.

# - url(r'^acc/', include('accblacksmith.urls', namespace='acc')),
urlpatterns = [
    ...
    url(r'^activate/(?P[-:\w]+)/$', ActivationView.as_view(), name='register_activate'),
]

from registration.backends.hmac.views import ActivationView as HmacActView
class ActivationView(HmacActView):
    
    def get_success_url(self, user):
        return ('acc:hmac:registration_activation_complete', (), {})


이제 user 가 email 에서 URL 을 click 하면, user 의 계정이 activate 되고, user 에게 activation complete 화면을 보여주게 된다.




기타 참고 자료


django 의 PasswordResetTokenGenerator 를 응용해서 email confirm 를 만들어 보자.

관련 view 를 살펴보면 아래와 같은 주석이 있다.
  • django/views.py at master · django/django · GitHub
    • # - password_reset sends the mail
    • # - password_reset_done shows a success message for the above
    • # - password_reset_confirm checks the link the user clicked and prompts for a new password
    • # - password_reset_complete shows a success message for the above


email 발송방법

email 발송은 아래를 참고하자.


django 의 smtplib.SMTP() 가 socket._GLOBAL_DEFAULT_TIMEOUT 을 사용하는데, 이 값이 default 가 설정되어 있지 않아서 smtplib.SMTP() 는 무한정 기다린다.



Reference

  1. django-registration 2.0 — django-registration 2.0.4 documentation



댓글 없음:

댓글 쓰기