はじめに
はじめに申し上げておきますと、私は5月ごろにプログラミングをはじめた初心者であり、もちろん同様にdjango
も初心者です。
ですので、他のプログラマーの方から見れば変なところで詰まっていると思われます。
しかし、ネットにあまり近しい事例が載っていないと感じたので、一部始終と解決策を書いていきたいと思います(チラリとdjango-rest-framework
の日本語記事が少ないというのも見かけたので、貢献できたらいいなとも考えてはいます)。
P.S.この丸一週間の苦しみは忘れないでしょう。
環境
Django
= 3.1.1
django-allauth
= 0.42.0
django-rest-auth
= 0.9.5
djangorestframework
= 3.11.1
流れと解決策
1.自分が定義したUser
モデルが、登録及び認証では使えない。
解決法
django
が提供するUser
をカスタムすることにより、自分のプロジェクトに近いUser
を作成
2.django-rest-auth
提供の登録画面でpassword1
とpassword2
を打っても、「この項目は必須です。」、つまり、空欄に扱われる。
解決法
RegisterSerializer
を編集して、必須項目を変更した。
3.登録画面を通して作成されたUser
が、ログインでは認証されない(non FieldError:与えらえた情報では認証できません
とのエラーメッセージが出る)。
解決法
RegisterSerializer
内の、User
をcreateする部分(メソッド)を書き換えた。
これが概要となります。
最後までご覧いただければ嬉しい限りです。
1. User
モデルが認証・登録で使えない
エラー概要
この問題が発生した原因は、__私が中途半端にログイン機能の実装方法を知っていたから__に他なりません。
-
私が知っていた実装法
-
User(models.Model)
から、独自にUser
モデルを作成 -
session
を用いて、session
内にuser_id
(ログイン時にDB内でフィルターをしてassignされる)があるかで、画面遷移を行う(エセ認証機能といったところでしょうか) -
故に、
api
で閲覧のみ許可といった、permissionが実装できない -
また、パスワードリセットといった方法がDB操作のみでしかできない
-
本来の実装法(一般的な実装法という方が近いかもしれません)
-
フレームワークが提供する
User
モデルを使用して認証を行う -
トークンを用いてログイン済みユーザーか判定
-
ログインしているかしていないかで、
api
の操作の制限を行える -
パスワードリセットといった操作がメール送信等で実装可能
上記のように、User
モデルの定義の仕方というスタートラインから私は勘違いをしていました。フレームワークがUser
を提供しているというのは微かに知っていましたが、__認証機能もそのフレームワークで提供されたUser
ではないと実装できない__というのが私の見解との大きな差であり、苦戦した原因でした。
おそらく、models.Model
で定義したUser
は認証に使えないので、認証機能を実装するのであれば、少々面倒ですがdjango
提供のUser
をカスタムする形でプロジェクトの最初から使ってください。
後に知りましたが、フレームワークを使うならフレームワーク組み込みのモデル・メソッドを使う必要があるので、その点は事前にチェックをしておいた方が良さそうです。
解決方法
実際に解決した流れを書いていきます。
参考にした記事はこちらです。
公式ドキュメント
[Django] カスタムユーザーを利用する
まず、カスタムユーザーを定義するにあたり必要なものは、BaseUserManager
, AbstractUser
, AbstractBaseUser
です。
-
__
BaseUserManager
は、User
作成周りの設定ができる__クラスです。管理ユーザー(この場合はauth
の方ではなくadmin
の方だと思います)作成の項目をusername
からemail
に変更したい場合等に使います(正直特にこのBaseUserManagerに関しては理解が浅いです、申し訳ないです)。 -
__
AbstractUser
__は__デフォルトのUser
が持ってる機能全て__を持っています。電話番号等少しだけ変更を加えたい場合に向いています。 -
__
AbstractBaseUser
__は__最低限の機能(パスワードのハッシュ化等)__を持っていて、大胆に変更をしたい場合に向いています。
私はフィールドを多く追加する予定でしたが、後のエラーを恐れ今回は__AbstractUser
__を使いました。尚、これら3つのクラスのソースコードはこちらとなります。
class CustomUserManager(UserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError('メールアドレスは必須項目です。')
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('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
基本的にほとんど元のコードのコピーをmodels.py
に貼り付けました。管理ユーザー作成に求められる項目をusername
からemail
に変えただけです。
class User(AbstractUser):
# AbstractUserでpasswordは定義されているため、ここではpasswordをしません(DBにはちゃんと保存されます。)
# 自分の要望にあわせて既に存在するフィールドも上書きをしています。
username = models.CharField(max_length=150, unique=True)
email = models.EmailField(max_length=100, unique=True)
profile = models.TextField(max_length=800, blank=True, null=True)
icon = models.ImageField(blank=True, null=True)
background = models.ImageField(blank=True, null=True)
# AbstractUserはfirst_name,last_nameを保持していますが、私には不要なので無効にしています
first_name = None
last_name = None
is_staff = models.BooleanField(
('staff status'),
default=True,
help_text=(
'管理サイトへのアクセス権の有無'),
)
is_active = models.BooleanField(
('active'),
default=True,
help_text=(
'ユーザーのアクティブ判定'
),
)
is_superuser = models.BooleanField(
('superuser status'),
default=True,
help_text=(
'Designates that this user has all permissions without '
'explicitly assigning them.'
),
)
# 後になって気づきましたが、date_joinedというのが既に`AbstractUser`では入っているのでcreated_atは必要ありませんでした。
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
#ここに上記のBaseUserManagerのクラス名を書きます
objects = CustomUserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELD = ["username", "email"]
class Meta:
db_table = "users"
これで、私が望んでいたUser
モデルを作成することができました。
しかし、ここで忘れてはならないことがあります。
AUTH_USER_MODEL = 'アプリ名.User'
これをしないと、作成したUser
モデルが認証ユーザーモデルとして設定されないので注意が必要です。
また、豆知識として、作成したUser
を1対多とかで関連付けたい場合は少しトリッキーで表記方法が変わります。実際の私のModel
を具体例として出すとこの様になります。
class Comment(models.Model):
comment = models.CharField(max_length=400)
owner = models.ForeignKey(
#
# Userはこの場合リレーションに使えません。
#
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="comment")
item = models.ForeignKey(Give_Item, on_delete=models.CASCADE, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
他のModel
と同じようにモデル名を素直に入れるわけではなく、__settings.AUTH_USER_MODEL
__と書かなければいけません。
これにて、自分の望み通りのUser
モデルをつかえて、それが認証にも使えるようになったわけです。次に認証周りに進みます。
2.django-rest-auth
でフィールドが認識されない
エラー概要
正直、これの原因については遂に解明できませんでした。しかし、1のカスタムユーザーを用いないで認証を実装する方法をいろいろ試していた段階で気付かぬうちに変に設定をいじっていたんだろうなと思います。
検証用で、全く異なる新規プロジェクトを作成したら同様のエラーが起きなかったので、ファイルのどこかに問題があったのでしょう(問題解明をしたかったです...)。
django-rest-auth
, django-all-auth
といった認証周りについては本記事ではほとんど説明しません。その代わり、私が参考にした記事を記載しておきます。
django-rest-auth を用いたユーザー認証の導入
私が遭遇したエラーは、ユーザー登録時においてpassword1
,password2
を確実に、しかも全く同じ文字を入力しているはずなのに、_「この項目は必須です」_と返ってくるというものです。
以下が実際の画面です(尚、POSTMAN
でも同様のコードをPOSTリクエストで送信しましたが同じレスポンスが返ってきました)。
このエラーに関しては、1のカスタムユーザーの作成に取り掛かる前から遭遇していたので、かなり苦しめられたものになります笑(説明しにくく、検索しても類似例がヒットしないというのが余計に難しくさせました)。
解決方法
実際の解決法について記載をしていきます。
ステータスコードが400なので、クライエント側に問題があったのは明白でした。
そこで、私が考え実行した解決策は__登録時の項目を変更する__というものです。
自分で設定ができれば上記の問題をコントロールできると考えたからです。また、パスワード確認はバックエンドで行わず、フロントエンド(私の場合はReact
です)のValidationのみでもできると判断をしたためです。
django-rest-auth
にて登録機能を担っているのはRegisterSerializer
です(django-rest-auth
が認証のメインですが、実装にはdjango-allauth
が必要です)。
公式ドキュメントとソースコードのリンクを貼っておくので興味がある方はご覧ください。
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=get_username_max_length(),
min_length=allauth_settings.USERNAME_MIN_LENGTH,
required=allauth_settings.USERNAME_REQUIRED
)
email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
#
# password1とpassword2を削除してpasswordフィールド一つのみにしています。
#
password = serializers.CharField(write_only=True)
def validate_username(self, username):
username = get_adapter().clean_username(username)
return username
def validate_email(self, email):
email = get_adapter().clean_email(email)
if allauth_settings.UNIQUE_EMAIL:
if email and email_address_exists(email):
raise serializers.ValidationError(
_("A user is already registered with this e-mail address."))
return email
def validate_password(self, password):
return get_adapter().clean_password(password)
def custom_signup(self, request, user):
pass
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password': self.validated_data.get('password', ''),
'email': self.validated_data.get('email', '')
}
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
adapter.save_user(request, user, self)
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
変更した箇所はシンプルで、password
を一つのフィールドのみにすることです。基本的に、必要な箇所のみを編集した形です。尚、ログインの方も私は設定したかったので、rest_auth.serializers.py
(ソースコード)からLoginSerializer
を編集しました(変更内容はログイン項目をusername
のみに変更するというものです)。
当初わたしは、このRegisterSerializer
を自分のアプリ内のapp.serializers.py
で上書きをしていました。しかし、RegisterSerializer
が色々なメソッドと結びついておりimport
のパス等で中々上手く変更が適用できなかったので、パッケージ内に直接変更を行いました。直感的に、パッケージへは直接変更をしない方がいいとは思うのですが、時間がかなりかかりそうなので今回はこの方法を採りました。皆さんは真似をしないでください。
もし、上書きをするのであれば、以下の様なコードをsettings.py
に書くことが必要です。
REST_AUTH_SERIALIZERS = {
'LOGIN_SERIALIZER': 'Serializerへのパス.LoginSerializer',
...
}
また、同時にSerializer
の名前をCustomLoginSerializer
等に変える場合、View
を変えるのもお忘れなく。具体的に書くとするなら、以下の様になると思います。
class LoginView(GenericAPIView):
"""
Check the credentials and return the REST Token
if the credentials are valid and authenticated.
Calls Django Auth login method to register User ID
in Django session framework
Accept the following POST parameters: username, password
Return the REST Framework Token Object's key.
"""
permission_classes = (AllowAny,)
#
# Serializerを変更後の名前に変更
#
serializer_class = CustomLoginSerializer
token_model = TokenModel
上記の変更を加えて、無事エラーは解決しユーザー登録をすることができました。
最後のエラーが待ち受けることを知らずに、私は一瞬ながら喜んでおりました。
3.登録画面を通したUser
がログイン時に認証されない
エラー概要
このエラーの原因は、私がRegisterSerializer
のフィールドを編集し、他のメソッドが参照できなかったために発生しました。
サブタイトルだけでは分かりづらいと思うので、詳しくエラーを説明していきたいと思います。
当然といえば当然ですが、django_rest_framework
はユーザー登録時にUser
クエリも作成してくれます。ユーザー登録へのURLはlocalhost:8000/rest-auth/registration/
なので、このURLへ必要項目と一緒にPOSTリクエストを送信すればユーザーも作成され、また、登録されるというわけです。
しかし、必要項目と一緒にlocalhost:8000/rest-auth/login/
へPOSTリクエストを送信しても_non FieldError提供された情報では認証を行えません
_と返ってきてログインが認証されません。
User
クエリを作成する方法はもう一つあり、それは認証ではなく、APIのエンドポイントを使うことです。すなわち、localhost:8000/api/user/
へPOSTリクエストを送信し、単純にcreateメソッドを用いてUser
を作成するということです。
後者の方法で、User
を作成しログインを試みたところ、User
は認証されトークンが発行されました。
これを受けて、私は以下の様に考えました。
ユーザー登録とAPIではUser
の作成方法が異なるのではないか
>APIによるcreateはUser.objects.create(...)
を用いたもので、この方法なら認証はされる
>>ユーザー登録時において、User
はUser.objects.create
以外で作成されている可能性がある
これを具体的にいうと、
__RegisterSerializer
がUser.objects.create
以外の方法でUser
クエリを作っている可能性がある__ということです。
解決方法
実際に解決した流れを書いていきます。
まずは、RegisterSerializer
のユーザー作成に関わるメソッドを見ていきます。
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
adapter.save_user(request, user, self)
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
この部分が登録時のユーザー作成を担っています。
adapter
については、公式ドキュメントから説明を読むことができます。 基本的に、登録周りの使用はdjango-allauth
の方のドキュメントを参照しなければいけません。
new_user(self, request): Instantiates a new, empty User.
save_user(self, request, user, form): Populates and saves the User instance using information provided in the signup form.
save_user
の方に、ロジックがありそうなのでsave_user
のメソッドを参照しにいきます。ソースコードはこちらです。
def save_user(self, request, user, form, commit=True):
"""
Saves a new `User` instance using information provided in the
signup form.
"""
from .utils import user_email, user_field, user_username
data = form.cleaned_data
first_name = data.get('first_name')
last_name = data.get('last_name')
email = data.get('email')
username = data.get('username')
user_email(user, email)
user_username(user, username)
if first_name:
user_field(user, 'first_name', first_name)
if last_name:
user_field(user, 'last_name', last_name)
#
# ここがpassword1となっているのが原因でした
#
if 'password1' in data:
user.set_password(data["password1"])
else:
user.set_unusable_password()
self.populate_username(request, user)
if commit:
# Ability not to commit makes it easier to derive from
# this adapter by adding
user.save()
return user
原因は、save_user
においてはパスワード生成はpassword1
が元となっていたからでした。
つまり、私がRegisterSerializer
のフィールドをpassword
に変更していたから発生したエラーです。
上記の箇所をpassword
に変更したところユーザー登録時に作成されたUser
も無事ログイン認証に成功しました。
以上が今回遭遇したエラーの全てです。
感想
今回の解決に時間がかかったのは、間違いなく最初からカスタムユーザーを使わなかったことです。一週間の最初の2,3日は、独自作成したUser
での実装方法を模索していたからです。最初から使っていれば、変にこねくり回すことなく2のエラーも起きなかったかもしれません(知識不足だったのもありますが)。
しかし、今回で学んだことは私自身としては大きかったです。
- 公式ドキュメントは細かいところまで目を通すこと
- パッケージのソースコードを場合によっては参照すること
- git reset --hard HEAD^
を乱用しない笑
知識不足もありましたが、たった一つの機能で、しかもそこまで難易度が高くないだろうものにここまで時間がかかるとは思っていませんでした。
ポートフォリオ作成に関しては、django-rest-framework
をこのまま使用していくので、日本語記事を増やすためにも気づいたことがあればアウトプット・共有をしていきたいと思います。
拙く、ひどく読みづらい、長い文章だったとは思いますが目を止めていただきありがとうございました。
間違っている箇所や、アドバイス等コメントをいただければ幸いです。