この記事は Django Advent Calendar 2017 20日目の記事です。
先日、満を持してバージョン2.0がリリースされました。
ただ、今回は新しい機能の紹介ではなく、1.xでも実装可能な「API認証」について書きます。
アジェンダ
- この記事を書こうと思ったきっかけ
- API認証の実装
- まとめ
この記事を書こうと思ったきっかけ
以前、こちらにも書いたのですが、私は自分がつくったシステムのログイン~ログアウト処理に、これまた自分で作ったGo(Gin)のAPIを利用しています。
※最近はソーシャルログインなど外部サービスとの連携が楽にできるのですが、自分の勉強のためにこのようにしています。
で、以前ASP.NETでつくったレシピ管理ツールを今年Djangoに移行しまして、そこでもAPI認証をすることにしました。
CakePHP3やLaravelでは比較的簡単にできたのですが、Djangoは少し癖があったので、備忘も兼ねて書き残しておこうと思ったのがきっかけです。
API認証の実装
手順は大きく4つです。
- 認証用ユーザモデルの作成
- 認証ミドルウェアの作成
- 認証バックエンドの作成
- 設定ファイルの変更
通常、Djangoではマイグレーション時にauth_user
テーブルが生成され、そこでユーザ情報を管理します。
グループやバーミッションなど細かい設定もできるのですが、今回はそれらは使用しません。
カスタムユーザモデルの作成
AbstratUser
を継承した、独自ユーザーモデルを作成します。
# models.py
""" Models """
from django.contrib.auth.models import AbstractUser
class ApiUser(AbstractUser):
""" API認証ユーザ """
REQUIRED_FIELDS = ['account', 'name', 'email']
is_staff = True
is_superuser = False
def get_access_token(self):
""" アクセストークン取得 """
return self.access_token if hasattr(self, 'access_token') else None
def get_full_name(self):
return self.fullname
@property
def is_authenticated(self):
return True if self.get_username() else False
def save(self, *args, **kwargs):
# DB保存はしないのでここの処理は実装しない
pass
def from_json(self, data):
""" JSONの値をオブジェクトに設定 """
self.pk = data.get('id')
self.access_token = data.get('accessToken')
self.username = data.get('account')
self.fullname = data.get('name')
self.email = data.get('mailAddress')
self.is_superuser = data.get('admin') # `admin`フラグがTrueなら管理者とする
return self
def to_json(self):
""" オブジェクトの値をJSONで取得 """
return {
'id': self.pk,
'accessToken': self.access_token,
'account': self.username,
'name': self.fullname,
'mailAddress': self.email,
'admin': self.is_superuser,
}
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
verbose_name = 'ログインユーザ'
verbose_name_plural = 'ログインユーザ'
abstract = False
managed = False
# 管理画面の認証も行う場合は必要
app_label = 'core'
db_table = 'auth_user'
セッションにsigned_cookie
を利用しているためJSON変換処理を用意しています。
カスタム認証ミドルウェアの作成
django.contrib.auth.middleware.AuthenticationMiddleware
を参考に、以下のようなカスタムミドルウェアを作成します。
# middlewares.py
""" Middleware """
import json
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
from app.models import ApiUser
class WebApiAuthenticationMiddleware(MiddlewareMixin):
""" API認証ミドルウェア """
def process_request(self, request):
"""
リクエスト時の処理
@param request
"""
assert hasattr(request, 'session'), (
"The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE%s setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'app.middlewares.WebApiAuthenticationMiddleware'."
) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
request.user = SimpleLazyObject(lambda: self.get_user(request))
def get_user(self, request):
"""
セッションからログインユーザを取得する
@param request
"""
from django.contrib.auth.models import AnonymousUser
user_data = request.session.get('user')
if not user_data:
return AnonymousUser()
# セッションから取得したデータをJSONに復元する
return ApiUser().from_json(json.loads(user_data))
認証バックエンドの作成
Django標準の認証バックエンドとしてModelBackend
(データベースを利用)やRemoteUserBackend
(ヘッダのユーザー名を利用)が用意されていますが、 今回はAPI認証のため独自のバックエンドファイルを用意します。
# backends.py
""" Backend """
import json
from logging import getLogger
import requests
from django.conf import settings
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from app.models import ApiUser
class WebApiBackend:
""" WebAPIを利用した認証クラス """
logger = getLogger(__name__)
def authenticate(self, request, username=None, password=None, **kwargs):
"""
認証処理
:param request
:param username
:param password
:param kwargs
:return ApiUser or None
"""
# ユーザIDとパスワードでAPI認証
auth_response = requests.post(
'http://api.example.com/auth', json={'account': username, 'password': password})
# アクセストークン取得エラー
if not auth_response.ok:
self.logger.error(auth_response.json())
return None
# 取得したアクセストークンからユーザ情報を取得
token = auth_response.json().get('accessToken')
user_response = requests.get(
'http://api.example.com/users', headers={'Authorization': 'Bearer %s' % token})
# ユーザ情報取得エラー
if not user_response.ok:
self.logger.error(user_response.json())
return None
user_json = user_response.json()
user_json['accessToken'] = token
user = ApiUser().from_json(user_json)
# 管理画面の認証も行う場合はここでデータベース側の登録・更新処理が必要
# シリアライズしたJSONデータをセッションに格納
request.session['user'] = json.dumps(user.to_json())
return user
@receiver(user_logged_out)
def deauthenticate(request, **kwargs):
"""
認証解除処理(ログアウト時のイベントフック関数)
:param request
:param kwargs
"""
user = getattr(request, 'user', None)
# APIの認証解除
if isinstance(user, ApiUser):
url = 'http://api.example.com/deauth'
response = requests.delete(
url, headers={'Authorization': 'Bearer %s' % user.get_access_token()})
if not response.ok:
logger = getLogger(__name__)
logger.error(response.json())
設定ファイルの変更
最後に、settings.py
を修正します。
# settings.py
…
MIDDLEWARE = [
...
'app.middlewares.WebApiAuthenticationMiddleware', # `AuthenticationMiddleware`の代わりに追加
]
AUTHENTICATION_BACKENDS = [
'app.backends.WebApiBackend', # 今回作成したバックエンドを追加
# 記載順に実行され、解決できない場合は後続が実行されるため、
# 他の認証バックエンドも利用する場合は後続に定義
]
AUTH_USER_MODEL = 'app.ApiUser' # 設定
以上で実装完了です。
まとめ
Djangoには標準で管理画面がついてきますが、こちらはDBありきの実装なので、認証・認可をAPIで乗り切ろうと思うとかなり大変です。
標準の管理画面を利用する場合は嫌でもデータベースを使うことになるので、その場合は素直にテーブル管理しましょう。
一応、上記コード上にもコメントで記載していますが、結局のところ認証ユーザとauth_user
テーブルを一旦同期させることになりそうです。
主な手順
ApiUser
をauth_user
として利用- API認証時、
auth_user
にレコードがなければ追加 - リレーションの維持にはその値を利用
この実装を通して、Djangoが認証をどのように行っているかを学べたのは良い経験でした。
まだまだ知らない機能が多いので、これからも機能追加しつつ楽しんで開発出来たらと思います。
一部記事用に修正しましたが、今回利用したソースコードの元はこちらにありますので、良ければご覧ください。