ModelSerializerを継承したシリアライザーからユーザーを登録、更新する際にパスワードをハッシュ化させる
頻繁に使うことになりそうなので備忘録として残しておく。
1.要約
ModelSerializerクラスが持つ登録時に呼び出すcreateメソッドと更新時に呼び出すupdateメソッドをオーバーライドする必要がある。
また、passwordに対してsettings.AUTH_PASSWORD_VALIDATORSで指定したバリデーターを使うようにすることも忘れてはいけない。
2.環境及び問題
Python 3.9.0
Django 3.1.7
Django REST Framework 3.12.4
最初にカスタムユーザーモデルを定義した。
長いがemailとpasswordを使って認証を行うカスタムユーザーモデルを定義しただけ。
例によってパスワードはハッシュ化して保存される。
from django.db import models
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
# create_user()とcreate_superuser()の共通処理
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError('a user must have an email address')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self.db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('a superuser must have is_staff=True')
if extra_fields.get('is_superuser') is not True:
raise ValueError('a superuser must have is_superuser=True')
return self._create_user(email, password, **extra_fields)
class User(PermissionsMixin, AbstractBaseUser):
email = models.EmailField(unique=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
objects = UserManager()
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
次にシリアライザーを定義する。
from rest_framework import serializers, fields
from users.models import User
.
.
.
class UserSerializer(serializers.ModelSerializer):
class Meta:
# 対象のクラス
model = User
# 利用するモデルのフィールド
fields = ['id', 'email', 'password']
id = serializers.IntegerField(read_only=False)
この状態でコンソール上で下記のようなJSONから新しいユーザーを作ってみることにする
{
"email": "[email protected]",
"password": "password@0123"
}
$ python manage.py shell
>>> from rest_framework.parsers import JSONParser
>>> from io import BytesIO
>>> from api.v1.serializers import UserSerializer
>>> from users.models import User
>>> data = JSONParser().parse(BytesIO('{"email": "[email protected]", "password": "password@0123"}'.encode()))
>>> serializer = UserSerializer(data=data)
>>> serializer.is_valid()
True
>>> new_user = serializer.save()
一見問題なさそうだが、実は既に問題が発生している。
>>> new_user.password
'password@0123'
なんとパスワードがハッシュ化されていない。
DBを直接確認しても生のパスワードが格納されてしまっていた。
また、UPDATEでも同じことが起きる
>>> data = {"email": "[email protected]", "password": "password@4567"}
>>> serializer = UserSerializer(instance=new_user, data=data)
>>> serializer.is_valid()
True
>>> updated_user = serializer.save()
>>> updated_user.password
'password@4567'
こちらもやはりDBには生のパスワードが格納されていた。
3.解決した方法
ModelSerializerには登録時に呼び出されるcreateメソッド、更新時に呼び出されるupdateメソッドが存在するのでこれらをオーバーライドし、passwordをハッシュ化する処理を盛り込む。
また、passwordへのバリデーション時にsettings.AUTH_PASSWORD_VALIDATORSで指定したバリデータを使うようvalidate_password()を定義する。
from django.contrib.auth import password_validation
from rest_framework import serializers, fields
from users.models import User
from destinations.models import Destination
class UserSerializer(serializers.ModelSerializer):
class Meta:
# 対象のクラス
model = User
# 利用するモデルのフィールド
fields = ['id', 'email', 'password']
id = serializers.IntegerField(read_only=False)
def validate_password(self, password):
"""入力JSONで指定されたpasswordに対してsettings.AUTH_PASSWORD_VALIDATORSで指定したバリデーションを実行"""
if password_validation.validate_password(password) is False:
raise serializers.ValidationError(f'The password {password} is not valid')
return password
# ユーザー作成時にパスワードを暗号化する
def create(self, validated_data):
# 後で使うので入力された生のパスワードを取得しておく
unhashed_password = validated_data.pop('password', None)
# パスワードを削除した入力データからUser型のインスタンスを生成
new_user = self.Meta.model(**validated_data)
# パスワードをハッシュ化してセットし、DBに保存
if unhashed_password is not None:
new_user.set_password(unhashed_password)
new_user.save()
return new_user
# ユーザー更新時にパスワードを暗号化する
def update(self, pre_update_user, validated_data):
# 更新されるユーザーのフィールドを入力データの値に書き換えていく
for field_name, value in validated_data.items():
# passwordを更新する際は入力データの値をset_password()の引数に渡してハッシュ化
if field_name == 'password':
pre_update_user.set_password(value)
# password以外のフィールドを更新する際は入力データでそのまま上書きでOK
else:
setattr(pre_update_user, field_name, value)
pre_update_user.save()
return pre_update_user
コンソールを再起動し、正しく動作するのを確認する。
なお、先ほど作成したパスワードがハッシュ化されていないデータは削除してある。
$ python manage.py shell
>>> from rest_framework.parsers import JSONParser
>>> from io import BytesIO
>>> from api.v1.serializers import UserSerializer
>>> data = JSONParser().parse(BytesIO('{"email": "[email protected]", "password": "password@0123"}'.encode()))
>>> serializer = UserSerializer(data=data)
>>> serializer.is_valid()
True
>>> new_user = serializer.save()
>>> new_user.password
'pbkdf2_sha256$216000$7oDDeRQFlgBh$NJ4wIO5QNEgtLCrqa/z00j+JevrVCYV+lZ/7SAoT6ig='
>>> data = {"email": "[email protected]", "password": "password@4567"}
>>> serializer = UserSerializer(instance=new_user, data=data)
>>> serializer.is_valid()
True
>>> updated_user = serializer.save()
>>> updated_user.password
'pbkdf2_sha256$216000$OE3a7dgg8gJc$WAYxiSNKAoIH7aK0Af/vISj3DpGQauaofTIkg+AKpVg='
登録時、更新時共にパスワードがハッシュ化されているのが確認できた。
参考
更新
- 2021/04/08
入力JSONのパスワードにバリデーションがかかってなかったのを修正。
入力JSONのパスワードにバリデーションがかかってなかったのを修正。
Author And Source
この問題について(ModelSerializerを継承したシリアライザーからユーザーを登録、更新する際にパスワードをハッシュ化させる), 我々は、より多くの情報をここで見つけました https://qiita.com/i05tream/items/4eb74d98d4d72299568a著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .