python

Djangoでログイン機能の実装

今回の実装ではクラスではなく、関数を用いて処理を書いていきます。
クラスはテンプレートを使用することで、簡単に色々な機能を実装できるメリットがあるんですが、裏側でどんな処理が走っているのか分からないのでカスタマイズがしにくいんですね。

そんな理由で、今回は関数を使用していきます。

Function Based Viewの実装

djangoのプロジェクトを立ち上げていきますが、プロジェクトの立ち上げ方が分からない方は、django入門その1を参照してみてください。
今回もアプリプロジェクトを作成した上での解説になります。私はloginprojectという名前でプロジェクトを作成して、loginappという名前でアプリを作成しています。

表示するためのtemplatesフォルダにsignup.htmlみたいな名前でファイルを作って、urls.pyの紐付けも行っておいてください。全てブログに書いているのでそちらをみてからの方が理解が早いと思います。

views.pyに実際の処理を書いていきます。
classでは、template_nameを使用していました。
関数では、最初から書かれているrenderメソッドを用いて、htmlファイルとどのモデルを使用するかの指定を行います。

ログイン機能実装前のサンプルとしてrenderの使い方をみておきます。
DBをまだ作成していないことを想定して、モデルは直接データを受け渡すようにしています。

views.py

from django.shortcuts import render

def signup(request):
  return render(request, 'signup.html', {'number': 100})

signup.html

{{ number }}

signup.htmlに諸々書き込んで行ってもいいのですが、今後のことも考えると共通のコンポーネントはbase.htmlに書き出しておきたいので変更します。
デザインも少し整えておきたいので、お馴染みのbootstrapを使用していきます。

base.html

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    {% block customecss %}
    {% endblock customecss %}

    <title>Hello, world!</title>
  </head>
  <body>
    {% block header %}
    {% endblock header %}

    {% block content %}
    {% endblock content %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

signup.html

{% extends 'base.html' %}

{% block content %}
<form class="form-signin" method="POST">
  <img class="mb-4" src="/docs/4.4/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72">
  <h1 class="h3 mb-3 font-weight-normal">Please sign up</h1>
  <label for="inputName" class="sr-only">UserName</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputEmail" class="sr-only">Email address</label>
  <input type="email" id="inputEmail" class="form-control" name="email" placeholder="Email address" required autofocus>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" class="form-control"  name="password" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
  <p class="mt-5 mb-3 text-muted">&copy; 2017-2019</p>
</form>
{% endblock content %}

これでざっくりしたデザインは整いました。

ここからようやく本題のログイン実装に移っていきます。

新規ユーザーの登録

formで送られた内容は、requestオブジェクトで受け取ることができます。
そこで、関数の引数にはrequestオブジェクトを受け取る設定をします。

フォームの内容を元にユーザーを作成していくわけですが、djangoはユーザーを作成するのに便利なモジュールを提供してくれています。
ある機能を実装したい時に、djangoが提供してくれているかを調べるには、自分で探してくる必要があります。qiitaとかを漁ってもいいのですが、できれば公式ドキュメントを調べる癖をつけた方がいいです。
今回のモジュール(ヘルパー関数)はここに載っています。
https://docs.djangoproject.com/ja/3.0/topics/auth/default/#creating-users

from django.shortcuts import render
from django.contrib.auth.models import User

def signup(request):
  if request.method == "POST":
    username = request.POST['username']
    email = request.POST['email']
    password = request.POST['password']
    user = User.objects.create_user(username, email, password)
  return render(request, 'signup.html', {'number': 100})

ここまでできたら、DBを作成して、書き込むめるかを試してみます。
ターミナルからmigrateしていきましょう。

python manage.py migrate

そうしたら、サーバーを立ち上げて確認していきます。
実際にDBに保存されているか確認するために管理画面に入って確認します。
管理画面へのアクセスの仕方はこちらを参考にしてください。
https://techpr.info/python/django-3/

実際に管理画面に入ると下記のようにユーザーが登録されていることが確認できます。

登録されていることは確認できましたが、この内容をどうすれば使えるかを見ていきます。

まず、User.objects を見ます。Userがテーブル名でobjectsが全てのデータです。
ではこれをpythonで標準出力させます。

from django.shortcuts import render
from django.contrib.auth.models import User

def signup(request):
  all_user = User.objects.all()
  one_user = User.objects.get(username='test2')
  print(all_user)
  print(one_user.email)
  if request.method == "POST":
    username = request.POST['username']
    email = request.POST['email']
    password = request.POST['password']
    user = User.objects.create_user(username, email, password)
  return render(request, 'signup.html', {'number': 100})

<QuerySet [<User: test2>, <User: test>]>
test2

のように表示されます。ユーザーのメールアドレスなどを取得したい場合は、
print(one_user.email)のようにカラムを指定してあげればできます。

重複ユーザ登録の禁止

django側で、同じ名前でユーザーを複数登録できないようにはしてくれてはいるのですが、デバッグ用のエラー画面が表示されよろしくありません。
実際のサービスでは、この仕様はよろしくないので修正していきます。

from django.shortcuts import render
from django.contrib.auth.models import User

def signup(request):
  if request.method == "POST":
    username = request.POST['username']
    email = request.POST['email']
    password = request.POST['password']
    # DB内のユーザー一覧に入力されたユーザー名が含まれているか確認
    try:
      # 合致するユーザーがなければDoesNotExistになる
      # except文に移行して、新規に登録が実行される
      User.objects.get(username=username)
      return render(request, 'signup.html', {'error': 'このユーザーは登録されています'})
    except:
      User.objects.create_user(username, email, password)
  return render(request, 'signup.html', {'number': 100})

html側でエラーがあった場合にはエラー文を表示させたいのでsignup.htmlも修正します。

{% extends 'base.html' %}

{% block content %}
<form class="form-signin" method="POST" action="">{% csrf_token %}
  <img class="mb-4" src="/docs/4.4/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72">
  <h1 class="h3 mb-3 font-weight-normal">Please sign up</h1>
  {% if error %}
    <div class="alert alert-danger" role="alert">
      {{ error }}
    </div>
  {% endif %}
  <label for="inputName" class="sr-only">UserName</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputEmail" class="sr-only">Email address</label>
  <input type="email" id="inputEmail" class="form-control" name="email" placeholder="Email address" required autofocus>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" class="form-control"  name="password" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
  <p class="mt-5 mb-3 text-muted">&copy; 2017-2019</p>
</form>
{% endblock content %}

ここまでできたらテストしましょう。
エラーの場合には下記のようになります。

ログイン画面の作成

これは、signup.htmlとほとんど同じなので、signin.htmlという名前で複製後、必要ない部分を削除していきます。
views.pyの方も似たようなものです。
一応、公式ドキュメントの該当箇所を載せておきます。
https://docs.djangoproject.com/ja/3.0/topics/auth/default/#how-to-log-a-user-in

views.py

from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login

def signup(request):
  if request.method == "POST":
    username = request.POST['username']
    email = request.POST['email']
    password = request.POST['password']
    # DB内のユーザー一覧に入力されたユーザー名が含まれているか確認
    try:
      # 合致するユーザーがなければDoesNotExistになる
      # except文に移行して、新規に登録が実行される
      User.objects.get(username=username)
      return render(request, 'signup.html', {'error': 'このユーザーは登録されています'})
    except:
      User.objects.create_user(username, email, password)
  return render(request, 'signup.html', {'number': 100})

def signin(request):
  if request.method == 'POST':
    # usernameを指定します。
    # emailを使用したい場合は、Userモデルをカスタマイズする必要があります
    username = request.POST['username']
    password = request.POST['password']
    # DBに存在するか確認
    user = authenticate(request, username=username, password=password)
    if user is not None:
      login(request, user)
      # renderでも可
      return redirect('signup')
    else:
      # 認証失敗時
      return redirect('signin')

  return render(request, 'signin.html')

signin.html

{% extends 'base.html' %}

{% block content %}
<form class="form-signin" method="POST" action="">{% csrf_token %}
  <img class="mb-4" src="/docs/4.4/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72">
  <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
  {% if error %}
    <div class="alert alert-danger" role="alert">
      {{ error }}
    </div>
  {% endif %}
  <label for="inputName" class="sr-only">UserName</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" class="form-control"  name="password" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
  <p class="mt-5 mb-3 text-muted">&copy; 2017-2019</p>
</form>
{% endblock content %}

redirectを使用するため、urls.pyを変更してname属性を追加します。
仮でsiginupにリダイレクトさせていますが、本来であればサービスページの方にリダイレクトさせます。

urls.py

from django.urls import path, include
from .views import signup, signin

urlpatterns = [
    path('signup/', signup, name='signup'),
    path('signin/', signin, name='signin'),
]

Userモデルで取り扱っているフィールドとデフォルト設定を軽くまとめました。
更に詳しい内容が知りたい人は公式ドキュメントに飛んでみてください。
https://docs.djangoproject.com/ja/3.0/ref/contrib/auth/#django.contrib.auth.models.User

フィールド必須説明
usernameoユーザ名。認証で使用。
passwordoパスワード。認証で使用。
emailメールアドレス。
first_nameファーストネーム。名前。
last_nameラストネーム。苗字。
groupsグループ。
user_permissions権限。
is_staffadmin サイトにアクセスできるか?
is_activeこのユーザは有効か?
is_superuserスーパーユーザか?
last_login最終ログイン日時。
date_joinedユーザ作成日時。

これで、ログイン認証の実装が完了しました。
ただし、実際のサービスではログイン状態を判定して、ページを出し分ける必要があります。
ここまでの実装では、ログイン状態じゃないとアクセスできないページがあったとして、アクセス先のURLを知っていれば、アクセスできてしまいます。
この辺りについては、次回以降に紹介していきますので少しお待ちください。

おまけ

renderとredirectについて

renderとredirectの挙動の違いについて説明していきます。
簡単に言ってしまうと、renderを使用するとURLと実際に表示されているhtmlの内容にズレが生じます。redirectはそれがありません。
sigin関数で使用している、redirectをrenderに書き換えて挙動の変化を見るのが一番手っ取り早いです。

from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login

def signup(request):
  if request.method == "POST":
    username = request.POST['username']
    email = request.POST['email']
    password = request.POST['password']
    # DB内のユーザー一覧に入力されたユーザー名が含まれているか確認
    try:
      # 合致するユーザーがなければDoesNotExistになる
      # except文に移行して、新規に登録が実行される
      User.objects.get(username=username)
      return render(request, 'signup.html', {'error': 'このユーザーは登録されています'})
    except:
      User.objects.create_user(username, email, password)
  return render(request, 'signup.html', {'number': 100})

def signin(request):
  if request.method == 'POST':
    # usernameを指定します。
    # emailを使用したい場合は、Userモデルをカスタマイズする必要があります
    username = request.POST['username']
    password = request.POST['password']
    # DBに存在するか確認
    user = authenticate(request, username=username, password=password)
    if user is not None:
      login(request, user)
      # renderでも可
      return render(request, 'signup.html')
    else:
      # 認証失敗時
      return redirect('signin')

  return render(request, 'signin.html')

この状態で、再度ログインしてみてURLと表示内容を見比べてみてください。
普段知っている挙動と違和感を覚えると思います。

なぜ、こう言うことが起こるかと言うと、renderはurlはそのままに呼び出すhtmlを変えているためです。
redirectは、引数に与えた名前からurls.pyを参照にしにいきます。そして、マッチしたurlがあれば再度リクエストしなおします。

こういった理由から挙動に変化が起きます。

CSSの適応

cssファイルを外部ファイルとして読み込みたいことがあるかと思うので、簡単に紹介しておきたいと思います。
signinページに外部cssを当てていきます。
そのためには、まずdjango側にどこにcssファイルが置いてあるかを認識させます。
その方法として、settings.pyに書き込んでいます。

settings.py

# ... ここまでに他の設定が書かれている ...

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

ここでは、staticフォルダという場所にcssファイルを置くことを明示的に示しています。
この名前は任意なので、好きに変えてもらって大丈夫です。
ただ、ここで設定したフォルダはプロジェクト立ち上げ時にはないので、manage.pyなどがある同階層に自身で作成する必要があります。

そして、settings.pyに書かれた情報をurls.pyでパターンマッチングすることで、html側からcssを呼び出すことができます。
注意点としては、1つ1つのcssファイルに対して、パスを書いていけば問題ないのですが、それはあまりに不毛なので、django側で一度に認識できる仕組みが用意してくれています。詳しくは公式ドキュメントを参照してみてください。
https://docs.djangoproject.com/en/3.0/howto/static-files/#serving-static-files-during-development

urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('snsapp.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

ここまで、できたらstaticフォルダの中にstyle.cssという名前でファイルを作成し、中身を書いていきます。

style.css

html,
body {
  height: 100%;
}

body {
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #f5f5f5;
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

最後に、signup.htmlで読み込んであるのですが、ここで少しお作法があります。
html側でもstaticディレクトリがある場所をdjangoに知らせる必要があり、下記のように書いていきます。

signup.html

{% extends 'base.html' %}
<!-- staticディレクトリがある場所をdjango側に知らせる -->
{% load static %}

{% block customecss %}
<link rel="stylesheet" href="{% static 'signin.css' %}">
{% endblock customecss %}


{% block content %}
<form class="form-signin" method="POST" action="">{% csrf_token %}
  <img class="mb-4" src="/docs/4.4/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72">
  <h1 class="h3 mb-3 font-weight-normal">Please sign up</h1>
  {% if error %}
    <div class="alert alert-danger" role="alert">
      {{ error }}
    </div>
  {% endif %}
  <label for="inputName" class="sr-only">UserName</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputEmail" class="sr-only">Email address</label>
  <input type="email" id="inputEmail" class="form-control" name="email" placeholder="Email address" required autofocus>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" class="form-control"  name="password" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
  <p class="mt-5 mb-3 text-muted">&copy; 2017-2019</p>
</form>
{% endblock content %}

{% load static %}以外は特質な部分はないです。
ここまでできたら、サーバーを立ち上げて確認するとデザインが変わっていると思います。
djangoでcssを外部ファイルとして扱えるっようになったので、デザインのカスタマイズ性が格段に上がりました。自身で色々と変更してデザイン性をあげて行ってみてください。