c-bata web

@c_bata_ のメモ。python多め

Djangoのユーザ認証まとめ

追記: 使用しているDjangoのバージョンをはじめ、いくつか古くなってきている点があります。ご注意ください

Djangoの認証に関する資料だと、今のところ id:nullpobug さんのPyCon JP 2017での発表もおすすめです。

tokibito.hatenablog.com

またOAuth認証に関していうと、python-social-authは少し余計なことをしすぎだなと今は感じています。 python-social-coreはうまく必要な機能を分離できているので、これで足りない部分を必要に応じて実装するのがいいかもしれません。参考までに。

github.com

追記終わり


Djangoでユーザ認証をしたかったので調べてみると,DjangoではUserモデルがはじめから用意されているらしい.そこでDjangoの提供する機能をそのまま使ってログイン・ログアウトを実装してみた. さらに調べてみるとpython-social-authというライブラリを使えば、TwitterFacebookGoogleGithub等のアカウントを使ったOAuth認証が簡単に実装できるみたいなのでそれも試しておく.

ソースコードGithubで公開してます.

環境はPython 3.4、Django 1.7を使用しています.

目次

Djangoの提供する機能でログイン・ログアウト

準備

まずプロジェクトを作成する。

$ django-admin.py startproject user_auth
$ python manage.py migrate
$ python manage.py createsuperuser
  : スーパユーザを作成
$ python manage.py runserver

ブラウザで http://127.0.0.1:8000/admin を開いてログイン出来るか確認.

うまく行ったなら,次はアプリケーションを作成する.認証用に独立したアプリケーションを用意したほうが良さそうなので,accountsというアプリケーションを作成した.

$ python manage.py startapp accounts

user_auth/settings.pyを開いてINSTALLED_APPSにアプリケーションを追加.ついでに言語とタイムゾーンの設定もしておく.

INSTALLED_APPS = (
    :
    'accounts',
)

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'


ログイン・ログアウトページの実装

djangoにはログイン・ログアウト用のviewがデフォルトで用意されているので,それをurls.pyに指定するだけでいい.

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^login/$', 'django.contrib.auth.views.login',
        {'template_name': 'accounts/login.html'}),
    url(r'^logout/$', 'django.contrib.auth.views.logout',
        {'template_name': 'accounts/logged_out.html'}),
)

ちなみに,registrationフォルダを作成して,その中にhtmlファイルを置く場合には,,{'template_name': 'path/to/template'}が省略できる. 今回はaccounts/templates/accounts/login.htmlに配置したため上記のように設定する必要がある.

  • accounts/templates/accounts/login.html
{% extends "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 'django.contrib.auth.views.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 %}
  • accounts/templates/accounts/logged_out.html
{% extends "base.html" %}

{% block title %}ログアウト{% endblock title %}

{% block content %}

    <h3 class="page-header">ログアウトしました</h3>

{% endblock content %}

このままだと,ログインフォームにユーザ名とパスワードを入力してログインしても,Page not foundが表示される.デフォルトでは,ログインに成功するとhttp://127.0.0.1:8000/accounts/profile/このようなURLにリダイレクトされるので,そこにユーザのプロフィールページ等を置く.

もし他の場所にリダイレクトさせたい場合は,settings.pyの中でLOGIN_REDIRECT_URLを指定する.


ログイン状態の確認

login_requiredデコレータを使えば,ページの閲覧をログイン済みのユーザだけに制限することが出来る. ログインしてないならsettings.pyLOGIN_URLに設定したパスに?next=requesst.pathを付けて飛ばす

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    ...

login_requiredデコレータを使わない場合は以下の様にする.

from django.http import HttpResponseRedirect

def my_view(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/login/?next=%s' % request.path)

ここまでの内容は↓のcommitのdiffを見るのが分かりやすいかと思います.

Implement login and logout · c-bata/django-auth-example@dbea95f · GitHub


Userモデルをカスタマイズ

参考

上の記事によるとDjango1.5以降ならAbstractUserモデルを実装すればいいらしい.

  • accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    image_url = models.URLField('画像URL', blank=True)
  • accounts/admin.py
from django.contrib import admin
from accounts.models import CustomUser

admin.site.register(CustomUser)
  • user_auth/settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'

実行

$ python manage.py makemigration
$ python manage.py migrate
$ python manage.py runserver

管理サイトにログインする.ログインできなくなっていた.コンソールで確認する.

>>> from accounts.models import CustomUser
>>> CustomUser.objects.all()
[]
>>> from django.contrib.auth.models import User
>>> User.objects.all()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/Users/masashi/.virtualenvs/itukalist/lib/python3.4/site-packages/django/db/models/manager.py", line 230, in __get__
    self.model._meta.object_name, self.model._meta.swapped
AttributeError: Manager isn't available; User has been swapped for 'accounts.CustomUser'

どうやらもう一度createsuperuserする必要があるらしい.

$ python manage.py createsuperuser
  : adminアカウントを作成

ログインしてみると今度はうまく行った! adminの中身を見てみると画像URLの項目ができていることが確認できる.

スクリーンショット 2015-01-10 22.29.08.png

他の確認方法

>>> from django.contrib.auth import get_user_model
>>> get_user_model()
<class 'account.models.CustomUser'>

補足

今回は試してないが,AbstractBaseUserを使うと自前で1から定義できるらしい. もし必要なら↓の記事が役に立ちそう.

Django 1.5のカスタムユーザーモデルとdjango-registration その2 - Misc Notes


Userモデルをキーとして新しいモデルを定義する

ブログやタスク管理アプリなどを作るなら,User以外にもモデルを定義しなければならない. ブログ記事やタスク等はUserモデルに紐付けたいため,Userモデルの外部キーとして定義する.

accountsアプリケーションは認証機能だけにしたいので,新たなアプリケーションを作ってそこにタスクモデルを作成する.

$ python manage.py startapp api
  • user_auth/settings.py
INSTALLED_APPS = (
    :
    'api',
)
  • api/models.py

Two Scoops of Django: Best Practices for Django 1.8 によると、settings.AUTH_USER_MODEL を使うらしい。 get_user_model() を使ってはいけない。importがloopするらしい。

from django.db import models
from user_auth import settings


class Task(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='tasks')
    summary = models.CharField('タスク', max_length=128)
    complete = models.BooleanField('状態', default=False)
    comment = models.CharField('コメント', max_length=512, blank=True)
    done_date = models.DateField('完了日', null=True, blank=True)

    def __str__(self):
        return str(self.id) + ': ' + self.summary
  • api/admin.py
from django.contrib import admin
from api.models import Task

admin.site.register(Task)

これらを追記したらマイグレーションして実行してみる.

$ python manage.py makemigration
$ python manage.py migrate
$ python manage.py runserver

管理システムにログインすると,Taskモデルが追加されている. 試しにUserモデルとTaskモデルのレコードを追加してみて確認したほうがいいかもしれない.


DjangoTwitterFacebookGoogleGithub認証

Python Social Authを使ってThird-party authenticationを行う.

準備

次はWebブラウザから確認するためのアプリケーションなのでwebという名前で作成する.

$ python manage.py startapp web
  • user_auth/settings.py
INSTALLED_APPS = (
  :
  'web',
)
  • web/views.py
from django.shortcuts import render_to_response
from django.template import RequestContext


def home(request):
    context = RequestContext(request,
                             {'user': request.user})
    return render_to_response('web/home.html',
                              context_instance=context)
  • web/templates/web/home.html

templatesディレクトリは無ければ作成.

{% extends "base.html" %}

{% block title %}ホーム{% endblock title %}

{% block content %}
    <div>
        <h1>ホーム</h1>

        <p>
            {% if user and not user.is_anonymous %}
                Hello {{ user.get_full_name|default:user.username }}!
            {% else %}
                ログインして下さい.
            {% endif %}
        </p>
    </div>
{% endblock content %}

base.htmlは,accounts/templates/からプロジェクトルート直下のtemplatesディレクトリ内に移動.

  • user_auth/urls.py

以下を追加

url(r'^$', 'web.views.home', name='home'),

ここまで追加したら動かしてみる. ログインしてからホームに行くと,ユーザ名が表示される.


python-social-auth

$ pip install python-social-auth
  • user_auth/settings.py

AUTHENTICATION_BACKENDSの中にFacebookとかGithubとか追加していけば良いけど, 登録が面倒なので今回はTwitterだけ.

INSTALLED_APPS = (
  :
  'social.apps.django_app.default',
)

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
    'django.core.context_processors.static',
    'django.core.context_processors.tz',
    'django.contrib.messages.context_processors.messages',
    'social.apps.django_app.context_processors.backends',
    'social.apps.django_app.context_processors.login_redirect',
)

AUTHENTICATION_BACKENDS = (
    'social.backends.twitter.TwitterOAuth',
    'django.contrib.auth.backends.ModelBackend',
)

LOGIN_REDIRECT_URL = '/'
SOCIAL_AUTH_TWITTER_KEY = 'Your Twitter Key'
SOCIAL_AUTH_TWITTER_SECRET = 'Your Twitter Secret'

補足

ログインはhref="{% url 'social:begin' 'twitter' %}"のようにすれば開始できるが,href="{% url 'social:begin' 'twitter' %}?next={{ next }}"のようにすれば,ログイン後nextで指定したパスにリダイレクトさせる事ができる.nextを指定しなかった場合settings.pyLOGIN_REDIRECT_URLにリダイレクトする.

  • web/templates/web/home.html
{% extends "base.html" %}

{% block title %}ホーム{% endblock title %}

{% block content %}
    <div>
        <h1>ホーム</h1>

        <p>
        <ul>
            {% if user and not user.is_anonymous %}
                <li>
                    <a>Hello {{ user.get_full_name|default:user.username }}!</a>
                </li>
                <li>
                    <a href="{% url 'auth:logout' %}?next={{ request.path }}">Logout</a>
                </li>
            {% else %}
                <li>
                    <a href="{% url 'social:begin' 'twitter' %}?next={{ request.path }}">Login with Twitter</a>
                </li>
            {% endif %}
        </ul>
        </p>
    </div>
{% endblock content %}
  • web/views.py
from django.shortcuts import render_to_response
from django.template import RequestContext


def home(request):
    context = RequestContext(request,
                             {'request': request,
                              'user': request.user})
    return render_to_response('web/home.html',
                              context_instance=context)

追記できたら、migrateしてrunserver. トップページからTwitterログインのボタンを押すと,認証が出来る. 認証後,管理サイトから確認すると自分のTwitterアカウントがCustomUserの中に追加されている.

終了!


終わりに

アカウント作成の手間を考えるとpython-social-authを使ってthird-party authenticationの方に利点がある.これからWebアプリ書く時はpython-social-authガンガン使っていこう.

↓の本にベストプラクティスが詰まっててとても良かった。

Two Scoops of Django: Best Practices for Django 1.8

Two Scoops of Django: Best Practices for Django 1.8