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')
知識ポイント
- 認証コードcodeはユーザー表には存在しないため、validateメソッドで処理後に削除
- initial_dataにはフロントエンドからのデータが含まれるため、辞書のように扱う:
self.initial_data['username'] - 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