Django REST frameworkとVueによる新鮮市場アプリ - ユーザー認証と登録

JSON Web Tokenによる認証処理

利用方法:http://getblimp.github.io/django-rest-framework-jwt/

インストール手順

pip install djangorestframework-jwt

設定方法

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    )
}

ルートURLの設定

from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    .......
    url(r'^login/', obtain_jwt_token),
]

Postmanでのテスト

http://127.0.0.1:8000/login/

これでログインが可能になります。

JWTのエンドポイントはデフォルトでユーザー名とパスワードでの認証を想定しており、電話番号でのログインでは失敗するため、カスタム認証を実装する必要があります。

カスタム認証の実装

JWTは内部的にauthモジュールのauthenticateメソッドを使用します。このメソッドをオーバーライドすることで対応できます。

(1) users/views.py

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q

User = get_user_model()

class CustomBackend(ModelBackend):
    """
    ユーザー認証をカスタマイズし、ユーザー名または電話番号でのログインを許可
    """
    def authenticate(self, username=None, password=None, **kwargs):
        print(username,password)
        try:
            # ユーザー名または電話番号でログイン可能
            user = User.objects.get(
                Q(username=username) | Q(mobile=username))
            if user.check_password(password):
                return user
        except Exception as e:
            return None

(2) settings.pyの設定

AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',
)

(3) JWTの有効期限設定

import datetime
# 有効期限
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

これにより、ユーザー名または電話番号でのログインが可能になります。

云片網によるSMS認証コード送信

(1) アカウント登録

"開発認証" -> "署名管理" -> "テンプレート管理"

IPホワイトリストも追加する必要があります。テスト時はローカルIPを使用し、本番環境ではサーバーのIPに変更してください。

(2) 認証コード送信

appsディレクトリにutilsフォルダを作成し、yunpian.pyファイルを作成します。以下のようなコードを記述します:

import requests
import json

class YunPian(object):

    def __init__(self, api_key):
        self.api_key = api_key
        self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"

    def send_sms(self, code, mobile):
        # 送信パラメータ
        parmas = {
            "apikey": self.api_key,
            "mobile": mobile,
            "text": "あなたの認証コードは{code}です。本人以外が操作した場合、本メッセージは無視してください。".format(code=code)
        }

        response = requests.post(self.single_send_url, data=parmas)
        print(response.text)
        re_dict = json.loads(response.text)
        return re_dict

if __name__ == "__main__":
    # 例:9b11127a9701975c734b8aee81ee3526
    yun_pian = YunPian("*****")
    yun_pian.send_sms("2018", "180****")

DRFによるSMS認証コード送信API

電話番号の検証:

  • 有効性の確認
  • 既に登録されているかの確認

(1) settings.pyでの電話番号正規表現の設定

# 電話番号の正規表現
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

(2) usersディレクトリにserializers.pyを作成

import re
from datetime import datetime, timedelta
from MxShop.settings import REGEX_MOBILE
from users.models import VerifyCode
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()

class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)
    
    # 関数名はvalidate + 検証対象フィールド名とする
    def validate_mobile(self, mobile):
        """
        電話番号の検証
        """
        # 登録済みか確認
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("ユーザーは既に存在します")

        # 有効性の確認
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("電話番号が無効です")

        # 送信頻度の確認
        # 60秒以内に一度のみ送信可能
        one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
            raise serializers.ValidationError("前回の送信から60秒経過していません")

        return mobile

シリアライザーのフィールド検証が可能です:

def validate_関数名(self, フィールド名):
    検証ロジック
    return 検証されたフィールド

(3) APIKEYの設定

# 云片網APIKEY
APIKEY = "xxxxx327d4be01608xxxxxxxxxx"

(4) viewsでのロジック実装

CreateModelMixinのcreateメソッドをオーバーライドします。以下のコードは元の実装です:

class CreateModelMixin(object):
    """
    モデルインスタンスを作成
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

カスタムロジックを追加します:

from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from .serializers import SmsSerializer
from rest_framework.response import Response
from rest_framework import status
from utils.yunpian import YunPian
from MxShop.settings import APIKEY
from random import choice
from .models import VerifyCode

class SmsCodeViewset(CreateModelMixin,viewsets.GenericViewSet):
    '''
    電話番号認証コード
    '''
    serializer_class = SmsSerializer

    def generate_code(self):
        """
        4桁の数字コードを生成
        """
        seeds = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))

        return "".join(random_str)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        # 検証
        serializer.is_valid(raise_exception=True)

        mobile = serializer.validated_data["mobile"]

        yun_pian = YunPian(APIKEY)
        # コード生成
        code = self.generate_code()

        sms_status = yun_pian.send_sms(code=code, mobile=mobile)

        if sms_status["code"] != 0:
            return Response({
                "mobile": sms_status["msg"]
            }, status=status.HTTP_400_BAD_REQUEST)
        else:
            code_record = VerifyCode(code=code, mobile=mobile)
            code_record.save()
            return Response({
                "mobile": mobile
            }, status=status.HTTP_201_CREATED)

知識ポイント

  • シリアライザーの取得:serializer = self.get_serializer(data=request.data)
  • 検証:serializer.is_valid(raise_exception=True)
  • 電話番号の抽出:mobile = serializer.validated_data["mobile"]

(5) URLの設定

from users.views import SmsCodeViewset

# codesのURL設定
router.register(r'code', SmsCodeViewset, base_name="code")

ユーザー登録インターフェースの実装

ユーザー登録には電話番号、認証コード、パスワードが必要です。CreateModelMixinを継承してモデル作成操作を実装します。

(1) UserProfileのmobileフィールド修正

mobile = models.CharField("電話",max_length=11,null=True, blank=True)

空値を許可します。フロントエンドではusernameのみが送信されるため、mobileは空でも問題ありません。

(2) users/serializers.pyに追加

from rest_framework.validators import UniqueValidator

class UserRegSerializer(serializers.ModelSerializer):
    '''
    ユーザー登録
    '''
    # UserProfileにはcodeフィールドがないため、独自に定義
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4,
                                 error_messages={
                                        "blank": "認証コードを入力してください",
                                        "required": "認証コードを入力してください",
                                        "max_length": "認証コード形式が無効です",
                                        "min_length": "認証コード形式が無効です"
                                 },
                                help_text="認証コード")
    # ユーザー名の重複確認
    username = serializers.CharField(label="ユーザー名", help_text="ユーザー名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="ユーザーは既に存在します")])

    # コードの検証
    def validate_code(self, code):
        # ユーザー登録時、POSTデータはinitial_dataに格納される
        # usernameは電話番号、認証コードは追加時間で降順ソート
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")

        if verify_records:
            # 最近の認証コード
            last_record = verify_records[0]
            # 有効期限は5分
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("認証コードの有効期限が切れています")

            if last_record.code != code:
                raise serializers.ValidationError("認証コードが間違っています")

        else:
            raise serializers.ValidationError("認証コードが間違っています")

    # 全フィールドの検証
    def validate(self, attrs):
        # フロントエンドからmobileは送られてこないため追加
        attrs["mobile"] = attrs["username"]
        # codeはDBに存在しないので削除
        del attrs["code"]
        return attrs

    class Meta:
        model = User
        fields = ('username','code','mobile')

知識ポイント

  1. 認証コードcodeはユーザー表には存在しないため、validateメソッドで処理後に削除
  2. initial_dataにはフロントエンドからのデータが含まれるため、辞書のように扱う:self.initial_data['username']
  3. validatorsでフィールドの重複をチェック:UniqueValidator(queryset=User.objects.all(), message="ユーザーは既に存在します")

(3) users/views.py

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    '''
    ユーザー
    '''
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

(4) URLの設定

from users.views import UserViewset

router.register(r'users', UserViewset, base_name="users")

Djangoシグナルによるパスワード変更処理

user/serializer.pyのUserRegSerializerにpasswordを追加

fields = ('username','code','mobile','password')

(2) パスワードの暗号化保存

Createメソッドをオーバーライドします:

# パスワード入力時に表示しない
    password = serializers.CharField(
        style={'input_type': 'password'},label=True,write_only=True
    )

    # パスワードの暗号化保存
    def create(self, validated_data):
        user = super(UserRegSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data["password"])
        user.save()
        return user

write_onlyの役割:返却データにはこのフィールドを含めない

codeフィールドを削除したため、シリアライズ時にエラーが発生する可能性があります。パスワードは表示せず、返却時にもシリアライズしないようにします。

シグナルの使用

(1) usersディレクトリにsignals.pyを作成

from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model

User = get_user_model()

@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()

(2) users/apps.py

from django.apps import AppConfig

class UsersConfig(AppConfig):
    name = 'apps.users'
    verbose_name = "ユーザー管理"
    def ready(self):
        import users.signals

user/serializer.pyのUserRegSerializerの以下のコードをコメントアウトします:

# パスワードの暗号化保存
    def create(self, validated_data):
        user = super(UserRegSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data["password"])
        user.save()
        return user

タグ: Django REST Framework JWT Vue SMS

7月24日 17:07 投稿