Djoser を触ってみたのでざっくりと実装について

参考

djoser について簡単に

  • アカウント周りの機能が揃っている。
  • 認証にJWTを使用している。
  • APIだけで完結する。

JWTを発行する

例えばですが、以下のようなログイン情報を送る事でJWTを返してくれるようです。

リクエスト

POST http://localhost:8888/api/auth/jwt/create/
content-type: application/json
Accept: application/json
{
    "email": "user1@example.com",
    "password": "helloworld1"
}

レスポンス

{
    "refresh": "rrrr",
    "access": "aaaa"
}

トークンが有効か確認する

token の値には access または refresh の値を指定するできます。
(レスポンスが {} なら有効。)

リクエスト

POST http://localhost:8888/api/auth/jwt/verify/
content-type: application/json
Accept: application/json
{
    "token": "aarr"
}

レスポンス

{}

このような感じでJWTを認証するエンドポイントも djoser が用意してくれているようです。
ただ認証が必要なAPIを叩く前に、毎回トークンが有効か確認するAPIを叩くのも大変かと思います。

なのでJWT認証が必要なAPIのヘッダにJWTを付与して、サーバー側で認証する方法を取ります。

JWTを付与したリクエスト

どうやらJWTは Authorization ヘッダに付与してリクエストを送るようです。

リクエスト

POST http://localhost:8888/api/hoge/
content-type: application/json
Authorization: JWT aaaa

JWT認証のデコレータ

方法で djangomiddleware もありますが、今回は decorator で実装してみました。
エラー処理はとりあえず省略しています。

def jwt_token_required(methods):
    def _jwt_token_required(f):
        def _wrapper(request, *args, **kwargs):
            try:
                if request.method not in methods: return f(request, *args, **kwargs)
                authorization_header = request.META.get('HTTP_AUTHORIZATION')
                # ヘッダ情報が不足している場合は例外とみなす。
                if not authorization_header: raise ValueError
                # JWTトークンの値のみ取得する。
                token = authorization_header.split()[1]
                # JWTトークンをデコードし有効性を検証する。
                jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
            except ValueError:
                # JWTトークンが提供されていません。
            except jwt.ExpiredSignatureError:
                # JWTトークンが期限切れです。
            except jwt.InvalidTokenError:
                # JWTトークンが無効です。
            except Exception:
                # 不正なリクエストです。
            return f(request, *args, **kwargs)
        return _wrapper
    return _jwt_token_required

ログイン

ログインのAPIも用意されています。
ログインに成功すると auth_token を発行し、ユーザー情報と紐づけてDBにトークンを格納し、フロントにこの値を返します。
なので「サーバーはこのトークンを受け取ってログイン中のユーザー情報を取得してください。」という事のようです。

リクエスト

POST http://localhost:8888/api/auth/token/login/
content-type: application/json
{
    "email": "user1@example.com",
    "password": "helloworld1"
}

レスポンス

{
    "auth_token": "atat"
}

auth_token を付与したリクエスト

ここら辺情報が見当たらなかったのでカスタムヘッダ auth-token にトークンを入れてリクエストを送ることにしました。

リクエスト

GET http://localhost:8888/api/hoge/
content-type: application/json
Authorization: JWT aaaa
auth-token: atat

auth_token 認証のデコレータ

こちらも decorator で実装してみました。
エラー処理はとりあえず省略しています。

djoser では「セッションIDを取り出して認証して、request に user を入れてくれる。」みたいなことはやってくれないっぽいので(多分)、デコレータで auth_token を取り出して認証します。成功したら request 属性に user を追加してあげます。

def auth_token_required(methods):
    def _auth_token_required(f):
        def _wrapper(request, *args, **kwargs):
            if request.method not in methods: return f(request, *args, **kwargs)
            try:
                request.user = User.objects.select_related("auth_token").get(auth_token__key=request.headers["auth-token"], is_active=True)
            except:
                # ユーザー認証に失敗しました。
            else:
                return f(request, *args, **kwargs)
        return _wrapper
    return _auth_token_required

認証エラーの対応

他のライブラリだと認証に失敗した時に、リダイレクト先を settings とかで指定する機能があったりしましたが、APIなのでここら辺も自力で実装が必要っぽいです。
自分はですが、上記の jwt_token と auth_token のデコレータ認証エラーが発生した時、レスポンスでステータス 401 を返すようにしました。

以下のように書くことでフロント側で、認証エラーの時のみ、ログイン画面にリダイレクトとかログインダイアログを表示の処理を書けそうですが、全部の axios にこれを書くのは大変なので axios.interceptor を使うことにしました。

(リダイレクト使ってしまうと djoser の良さがない気もしますが。)

axios.post(`/api/hoge/`, params, getHeader()).then((response) => {
    // 成功
}).catch((error) => {
    // 失敗
    if (error.response.status == 401) {
        // 認証系エラーの処理をする。
        // 例えばログイン画面にリダイレクトするとかログインダイアログを出すとか。。
    } else {
        // その他のエラー処理をする。
    }
});

以下は axios.interceptor に書き換えた例です。すべてのリクエストの後に、ステータスが 401 なら xxx する、といった処理を入れてくれるようです。

axios.interceptors.response.use(
      (response) => {
        return response;
      },
      (error) => {
        if (error.response.status == 401) {
            // 認証系エラーの処理をする。
            // 例えばログイン画面にリダイレクトするとかログインダイアログを出すとか。。
        }
        return Promise.reject(error);
      }
    );

メール認証まわり

「アカウント登録からの有効化」や「パスワードリセット要求」時にはメールが飛ぶかと思います。

例えばデフォルトのURLだと以下のようなエンドポイントが用意されています。
これはアカウント登録時に送信されるメールに付与されたURLを解決するもので、アカウント有効化のエンドポイントです。

urlpatterns = [
    re_path(r'^activate/(?P<uid>[\w-]+)/(?P<token>[\w-]+)/$', views.Index.as_view(), name="activate"),
]

例えばこのURLをメールからクリックして「有効化しました。」のページに飛んだ後に、メインページにリダイレクトとかボタンを押してメインページに遷移すると、これもAPIの意味が薄れてしまう気がしたので、そのまま views.Index.as_view でメインのページに遷移するように指定しました。

遷移後に「/activate/hogehoge/fugafuga」を残さないようにフロント側のルータでURLを書き換えて、メインのページのULR例えば「/」とするように実装しました。

(つまり何とか redirect や return render を避けるようにしたということです。。。)

感想的な

一通り触ってUIも実装してみましたが、他のユーザー認証系ライブラリに比べると少し手がかかると思いました。

サーバー側は、djoser が用意してくれたエンドポイント以外は、認証する仕組み(今回のデコレータ)の部分を作成しないといけなそうですし、

フロント側は、今回あまり記事に載せていませんが、jwt と auth のトークンの発行と保持、送信で結構コードが多くなってしまいました。

リクエスト一覧

djoser のリクエストを VsCode のクライアントから簡単に送れるように前の記事にまとめてあります。