[Django]Rest Framework JWTでのパーミッション


はじめに

Django Rest Framework JWTを使用して認証を行なっている時、パーミッション関連で詰まったのでそのまとめです。

使用ライブラリ・フレームワーク

クライアントサイドはAngular、APIサーバーとしてDRFを利用していました。
ユーザー認証ではDjango Rest Framework JWT(以下 DRF JWT)を利用していました。
DRF JWTについては過去に投稿しているので、そちらもよければ。

やりたかったこと

ユーザーごとに権限レベル(ex. 管理者、一般ユーザー等)が設定されており、それによってユーザーができる操作(POST,DELETE等)を制限することが目標でした。
(ex. 管理者権限を持ったユーザーはユーザーの追加・変更・削除ができるが、権限を持たないユーザーは閲覧しかできない等)

詰まった内容について

request.userがAnonymous

viewに渡されるrequest.userからユーザー情報を取得し、権限を判定したかったのですが、JWTでトークンを取得しているにもかかわらず、ユーザー情報が入ってきませんでした。

その理由は以下の通りです。

  • HttpRequestにuserを設定しているのは、DjangoのAuthenticationMiddleware。ユーザーがログインしているならそのユーザーの情報を、ログインしていないならAnonymousUserを設定する。
  • AuthenticationMiddlewareはセッションIDからユーザーを識別し、request.userに設定している。
  • セッションIDが保持されるのはDjangoのlogin関数を呼び出した時。
  • トークンの発行はrest_framework_jwt.views.obtain_jwt_tokenによってなされる。この関数はセッションを保持しないしlogin関数も呼ばない。

つまり、DRF JWTでトークンを取得しただけではセッションが保持されないので、request.userにユーザー情報は入ってきません。

どうしたか

権限の判定が必要な部分でトークンからユーザー情報を取得する方針に切り替えました。
リクエストのヘッダーにAuthorization: JWT <トークン>という形式で取得したトークンを設定しています。これを取得し、ユーザーを取得します。
なお、こちらのブログを参考にさせていただきました。

permission.py
from functools import wraps
from django.http import JsonResponse
from django.contrib.auth.models import AnonymousUser
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer

def check_user_auth(method, auth):
    def decorator(view_func):
        @wraps(view_func)
        def _decorator(request, *arg, **kwargs):
            if isinstance(method, list):
                methods = method
            else:
                methods = [method]

            # unrestricted method
            if request.method not in methods:
                return view_func(request, *arg, **kwargs)

            user = _get_user_from_token(request)

            # anonymous user
            if not hasattr(user, 'authorization'):
                return JsonResponse({
                    'auth': False
                }, status=status.HTTP_401_UNAUTHORIZED)

            if isinstance(auth, list):
                auths = auth
            else:
                auths = [auth]

            if user.authorization.auth in auths:
                return view_func(request, *arg, **kwargs)

            # does not have permission
            return JsonResponse({
                    'auth': False
                }, status=status.HTTP_401_UNAUTHORIZED)
        return _decorator
    return decorator

def _get_user_from_token(request):
    token = request.META.get('HTTP_AUTHORIZATION', ' ').split(' ')[1]
    try:
        valid_data = VerifyJSONWebTokenSerializer().validate({'token': token})
        return valid_data['user']
    except ValidationError:
        return AnonymousUser()

使い方としては@check_user_auth('POST', <権限>)のような形でviewのデコレータして使います。
やっていることとしては,

  • 制限をかけているメソッドか判定。
  • トークンからユーザーを取得。できなかった場合はAnonymousUserとして扱う。
  • userモデルをカスタマイズしてauthorization.authに権限を持つようにしているので、トークンから取得したユーザーのauthorization.authが許可された権限に含まれているか判定。
  • 許可されているユーザーならviewを実行、不許可なら401を返す。

ということをやっています。

終わりに

どうしてAnonymousしか入ってこないかで結構詰まりました。
ミドルウェアの仕組みを理解していればもっと早く解決できたはず...。

色々調べた結果ですが、誤解や認識不足がありましたらご指摘願います。
また、すでに用意されている方法や、もっとうまいやり方などありましたらご教授ください。