0
1

More than 3 years have passed since last update.

ModelSerializerを継承したシリアライザーからユーザーを登録、更新する際にパスワードをハッシュ化させる

Last updated at Posted at 2021-04-04

頻繁に使うことになりそうなので備忘録として残しておく。

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を使って認証を行うカスタムユーザーモデルを定義しただけ。
例によってパスワードはハッシュ化して保存される。

project/users/models.py
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'

次にシリアライザーを定義する。

project/api/v1/serializers.py
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": "foo@bar.com",
  "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": "foo@bar.com", "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": "bar@baz.com", "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()を定義する。

project/api/v1/serializers.py
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": "foo@bar.com", "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": "bar@baz.com", "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='

登録時、更新時共にパスワードがハッシュ化されているのが確認できた。

参考

更新

  1. 2021/04/08
    入力JSONのパスワードにバリデーションがかかってなかったのを修正。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1