はじめに
いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第2弾です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
先日の #モグモグDjango で確認したことをブログにまとめました。
背景
Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。
Django 2系に書き換える対象は、3ページあります。
- Homework: add more to your website! · Django Girls Tutorial: Extensions (機能追加)
- Homework: secure your website · Django Girls Tutorial: Extensions (セキュリティ)
- Homework: create comment model · Django Girls Tutorial: Extensions (コメント機能)
今回はセキュリティのチュートリアルを進める中で気づいたことを書きます。
ブログアプリで何が問題か
Django Girls Tutorialの中で、ログインしているユーザだけに投稿作成ボタンや編集ボタンが見えるようにしました。
参照:Djangoフォーム · Django Girls Tutorial の「セキュリティ」
これは新しい投稿の作成を完全に保護するものではありませんが、それは良い第一歩です。 私たちは拡張レッスンでより多くのセキュリティをカバーします。
確かに画面上にボタンが見えなくなったのですが、URLを推測すると記事が作れてしまうという問題があります。1
ログインしていないユーザでhttp://127.0.0.1:8000/post/new/
にアクセスすると、記事の作成画面が表示されます。
セキュリティ対応
ログインが求められるように修正
URLを推測されても記事が作成できないようにする対応はそれほど大変ではありませんでした。
ビューの関数に@login_required
を設定すると、ログインが求められるように変更できます。
blog/views.py
from django.contrib.auth.decorators import login_required # 追加 from django.shortcuts import redirect # 省略 @login_required # 追加 def post_new(request): # 省略 @login_required # 追加 def post_edit(request, pk): # 省略 @login_required # 追加 def post_draft_list(request): # 省略 @login_required # 追加 def post_publish(request, pk): # 省略 @login_required # 追加 def post_remove(request, pk): # 省略
この時点ではログイン画面のテンプレートが用意されていないため、ログインしていないユーザで、http://127.0.0.1:8000/post/new/
(記事作成のURL)にアクセスするとエラーとなります。
ですが、誰でも記事が作れる状態ではひとまず修正されました。
ログイン画面を用意する
ログイン画面用のURLの設定は、プロジェクト側で(mysite/urls.py
で)行います。
# 省略 from django.contrib.auth import views # 追加 urlpatterns = [ path('admin/', admin.site.urls), path('accounts/login/', views.login, name='login'), # 追加 path('', include('blog.urls')), ]
ログイン画面のテンプレートを追加しましょう。
blog/templatesの下にregistration
というフォルダを作り、login.html
を置きます。
ログイン機能はDjangoに用意されたものを使うために、命名規則を守る必要があると理解しました。
blog/templates/registration/login.html
{% extends "blog/base.html" %} {% block content %} {% if form.errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} <form method="POST" action="{% url 'login' %}"> {% csrf_token %} <table> <tr> <td>{{ form.username.label_tag }}</td> <td>{{ form.username }}</td> </tr> <tr> <td>{{ form.password.label_tag }}</td> <td>{{ form.password }}</td> </tr> </table> <input type="submit" value="login"> <input type="hidden" name="next" value="{{ next }}"> </form> {% endblock %}
テンプレートで使われているform.username.label_tag
は、HTMLのlabel要素を作る関数だそうです。
https://docs.djangoproject.com/en/2.0/topics/forms/#rendering-fields-manually
ログインしていないユーザで、http://127.0.0.1:8000/post/new/
(記事作成のURL)にアクセスすると、以下のようなログイン画面が表示されます!
(ただし、画像はログイン画面に直接アクセスしたときのものです。)
ログイン画面周りでは、あと少しだけコードを修正する必要があります。
ログイン画面のURL(http://127.0.0.1:8000/accounts/login/
)を直接叩いてログインした場合、ログインしたあとにページが見つからずにエラーとなります。
この対応はmysite/settings.py
で行います。
末尾に以下の1行を追加しました。
LOGIN_REDIRECT_URL = '/'
この設定により、ログイン画面のURLを直接叩いた場合、ログインに成功後、記事一覧(http://127.0.0.1:8000/
)が表示されます。
ログイン・ログアウトのためのリンクを追加
ログイン機能を使うためのリンクを追加します。
blog/templates/blog/base.html
{% if user.is_authenticated %} <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a> <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a> {% else %} <!-- 追加 --> <a href="{% url 'login' %}" class="top-menu"><span class="glyphicon glyphicon-lock"></span></a> <!-- 追加 --> {% endif %}
ログインしていない場合、ヘッダーにログイン画面に遷移するリンク(鍵のアイコン)が表示されます。
ログアウトの機能も実装します。
mysite/urls.py
# 省略 urlpatterns = [ path('admin/', admin.site.urls), path('accounts/login/', views.login, name='login'), path('accounts/logout/', views.logout, name='logout', kwargs={'next_page': '/'}), # 追加 path('', include('blog.urls')), ]
blog/templates/blog/base.html
{% if user.is_authenticated %} <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a> <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a> <p class="top-menu">Hello {{ user.username }} <small>(<a href="{% url 'logout' %}">Log out</a>)</small></p> <!-- 追加 --> {% else %} <a href="{% url 'login' %}" class="top-menu"><span class="glyphicon glyphicon-lock"></span></a> {% endif %}
ログインユーザの情報とログアウトのリンクがヘッダーに表示されるようになりました。
書き換えなくてもDjango2.0.9で動きました
ここまででDjango2系向けに書き換えたのは、urlpatternsのurl関数です(path関数にしました)。
Django2系でもurl関数は使えるので、Extensionsのコードのままでも動きました。
参考:Django 1.11 と 2.0 の違い (「現場で使える 基礎 Django」本の補講その1) - akiyoko blog
django.conf.urls.url() を使った書き方は、Django 2.0 でもそのまま使えるように互換性が保たれていますし、(後略)
mysite/urls.py
from django.conf.urls import url # 追加 from django.contrib import admin from django.urls import path, include from django.contrib.auth import views # 追加 urlpatterns = [ path('admin/', admin.site.urls), url(r'^accounts/login/$', views.login, name='login'), # 追加 url(r'^accounts/logout/$', views.logout, name='logout', kwargs={'next_page': '/'}), # 追加 path('', include('blog.urls')), ]
mysite/urls.pyをDjango2系向けにアップデート
この記事を書く中で記述の食い違いに気づきました。
- 原文(Django 1系):
django.contrib.auth.views.login
- セキュリティのバートで参照されているドキュメント2の書き方:
django.contrib.auth.login
結論を言うと、mysite/urls.pyを以下のように書き換えた方がよいことがわかりました。
# 省略 urlpatterns = [ path('admin/', admin.site.urls), path('accounts/login/', views.LoginView.as_view(), name='login'), # 変更 path('accounts/logout/', views.LogoutView.as_view(next_page='/'), name='logout'), # 変更 path('', include('blog.urls')), ]
これはDjango 2系で動く書き方です。
LoginView()
を使う書き換えをしないと、Django 2.1.xで動かないと考えています。3
(django.contrib.auth.views.login
を使った書き方は、Django 2.0.xまでで動く書き方で、2.1.xでは動かないという認識です)
LogoutView.as_view()
では、引数にログアウト後に表示されるページ(記事一覧)のURLを渡しています。4
mysite/settings.pyにLOGOUT_REDIRECT_URL = '/'
を指定することでも同じ動きが実現できましたが、Django Adminからログアウトした場合も記事一覧に遷移するという副作用がありました。
なお、django.contrib.auth.login
を使う書き方はビュー向けであるようで、importしてからpath('accounts/login/', login, name='login')
のように使うとエラーとなりました。(TypeError: login() missing 1 required positional argument: 'user'
)
終わりに
Extensionsでログイン機能を触ったことで、Djangoでアプリケーションが作れそうな気がしてきました。
そして、残るはコメント機能。
Django2系の動作確認最終回でまたお会いしましょう。
最後までお読みいただき、ありがとうございました。
脚注
-
PythonAnywhereで公開されているDjango Girls Tutorialのブログアプリは第三者が記事を作れる状態なので、なるべくExtensionsのセキュリティのパートまで進めていただくのを個人的には推奨します。↩
-
ドキュメントのURLのうち、Djangoのバージョンだけ2.0に変えて参照しました:https://docs.djangoproject.com/en/2.0/topics/auth/default/#how-to-log-a-user-out↩
-
django.contrib.auth.views.login
のソースを見たところ、'The login() view is superseded by the class-based LoginView().'
(RemovedInDjango21Warning
)と記載されていました。Django 2.1.xではLoginView()
を使う書き方に置き換わるようです。→リリースノートを確認したら削除されていました:https://docs.djangoproject.com/ja/2.1/releases/2.1/#features-removed-in-2-1↩