🫠

Django Restframework でのメール認証機能追加

2024/03/09に公開

概要

フロント側を Vue.js で実装し, バックエンド側を Django REST Framework で実装している.
メールでの通知機能をつけるためアカウント作成時にメールアドレスを登録必要があったけど, 自分が使用していないメールアドレスでも登録可能だったので認証できるようにしたかった.
バックエンド側を中心に書いていく.

作ったもの

ログイン画面. メールアドレスとユーザー名, パスワードを入力

サインアップボタンを押すとアカウントを作成するバックエンド側のAPIにリクエストを送る.
このあと, 入力したメールアドレスに対して以下のようなメールが送信される.

このメールは Essence Catch から送信されています。
以下のリンクをクリックしてアカウントを有効化してください。
https://essence-catch.com/activation/?token=<認証用トークン>

このメールに心当たりがない場合は、このメールを無視してください。
また, 送信専用のアドレスのため, このメールに返信しないでください。

EssenceCatch © 2024 by Kaito.

記載されているURLにアクセスすると認証完了の画面が表示される.

必要な準備

本番環境にはAWS SES を使用してメールを送信するがこちらの設定が必要.
Django ではデバックのためにコーンソール上にメールを表示することが可能なため今回の説明にはそちらを使用する.

コンソールにメールを表示させるために setting.py に以下の設定を追記
EMAIL_BACKEND = django.core.mail.backends.console.EmailBackend

これで send_mail 関数を実行するとメールが送信される.

send_mail(subject, # メールのタイトル
          message, # 送信するメッセージ
          from_mail_address, # 送信元メールアドレス
          [email]) # 送信先のメールアドレス

実装

メール認証の流れとしては

  1. フロント側からユーザー作成のリクエスト
  2. バックエンド側の view でユーザーを作成し, それに紐づいたアクティベーショントークンを作成
  3. アクティベーション用URLを添付したメールを送信
  4. アクティベーション用のページからバックエンド側へアクティベーションのリクエストを発行
  5. 認証完了

モデルの作成

以下の様のモデルを作成する

class AccountActivateToken(models.Model):
    token = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                on_delete=models.CASCADE,
                                related_name='activate_token')
    activate_token = models.UUIDField(default=uuid.uuid4)
    expired_at = models.DateTimeField()
    objects = AccountActivateTokensManager()

    def set_expiration_date(self):
        self.expired_at = timezone.now() + timedelta(days=10)
        self.save()
        return self.expired_at

更にアクティベーションとトークン作成用にマネージャーも追加しておく.

class AccountActivateTokensManager(models.Manager):

    def activate_user_by_token(self, activate_token):
        # 有効期限内のトークンを取得
        user_activate_token = self.filter(
            activate_token=activate_token,
            expired_at__gte=timezone.now()  # 有効期限内のトークンを取得
        ).first()

        # トークンが存在する場合はアカウントを有効化する
        if user_activate_token:
            user = user_activate_token.user
            user.is_active = True
            user.save()
            return user
        else:
            raise self.model.DoesNotExist

    def create_token(self, user):
        # トークンを作成
        token = self.model(user=user)
        token.set_expiration_date()
        token.save()
        return token

メールの送信

入力したメールアドレスやユーザーIDが正しいことを確認したあとにメールを送信する.

    def send_activation_email(self, user):
        """
        ユーザーにアクティベーションメールを送信します。
        """
        # 送信先アドレス
        email = user.email

        # アクティベーショントークンを作成
        account_activate_token = AccountActivateToken.objects.create_token(user=user)
        activate_token = account_activate_token.activate_token

        # アクティベーションURLを作成
        url = f'{ACTIVATE_URL}/activation/?token={activate_token}'

        # タイトル
        subject = 'Essence Catch : Account Activation'

        # メッセージ
        message = f'''
        このメールは Essence Catch から送信されています。
        以下のリンクをクリックしてアカウントを有効化してください。
        {url}
        
        このメールに心当たりがない場合は、このメールを無視してください。
        また, 送信専用のアドレスのため, このメールに返信しないでください。
        
        
        EssenceCatch © 2024 by Kaito.
        '''.format(url=url)

        send_mail(subject,
                  message,
                  self.from_mail,
                  [email])

アカウントの有効化

アクティベーション用のviewでトークン認証する.
成功したら 200 を返してフロント側で成功と表示させる.

user = AccountActivateToken.objects.activate_user_by_token(token)
# アカウントが有効化された時
if user is not None:
    logger.info('アカウントが有効化されました')
    # user に紐づくアクティベーショントークンを削除
    AccountActivateToken.objects.filter(user=user).delete()
    return Response(
        {'message': 'Account activated successfully'},
        status=status.HTTP_200_OK)

参考にしたサイト

https://qiita.com/koseidaiki/items/85279375555a533c9318

https://qiita.com/shun198/items/c825af382a9be8c2fec7

https://blog.css-net.co.jp/entry/2023/03/28/112402

Discussion