はじめに
Flaskで作ったアプリケーションにJWTによるトークンでの認証機能を実装したので、
その時に調べたことなどをまとめました。
この記事に書いてあること
- JWTとは何か
- JWTによる認証のメリット、注意点
- Flask-JWTを使って、トークンによる認証機能を実装する方法
JWTとは
JSON Web Tokenの略で、「ジョット」と読みます。
JWTは署名の出来るJSONを含んだURL Safeなトークンで、
主に次の特徴があります。
- 発行者だけが、秘密鍵を使ってトークンが正しいことを検証できる
- JSONの中身は誰でも見ることができる
- トークンはBase64でエンコードされただけの文字列
- 署名により、JSONの改ざんを検知できる
JWTによる認証のメリット、注意点
メリット
ステートレス
JWTによるトークン認証における大きなメリットは、ステートレスであることです。
このことによって、以下のような恩恵を受けることができます。
- ユーザーが増えた場合、サーバー側で認証情報を保持しないので、メモリの心配をしなくて済む
- サーバーが複数台でもログイン状態を保持できる
- テストの工数が減る(状態があると、状態によってテストケースが増える)
- サービスをまたいだ認証機構として利用できる
注意点
重要な情報をJSONに含めない
冒頭でも説明した通り、JSONの中身はBase64でエンコードされただけの文字列です。改ざんはできませんが、中身を確認することは誰でもできます
Flask-JWTとは
Flask-JWTはFlaskアプリケーションでJWTの機能を使用できるライブラリです。
ドキュメントはこちら
ここからは、Flask-JWTを使って
トークンを使った認証機能を実装する手順をご紹介します。
最小構成
root
└ app.py
インストール
まずはFlaskとFlask-JWTをインストールします。
pip install Flask Flask-JWT
アプリケーション
次はアプリケーションの中身ですが、こちらはFlask-JWTのドキュメントから。
from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp
class User(object):
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password = password
def __str__(self):
return "User(id='%s')" % self.id
users = [
User(1, 'user1', 'abcxyz'),
User(2, 'user2', 'abcxyz'),
]
username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}
def authenticate(username, password):
user = username_table.get(username, None)
if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')):
return user
def identity(payload):
user_id = payload['identity']
return userid_table.get(user_id, None)
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'super-secret'
jwt = JWT(app, authenticate, identity)
@app.route('/protected')
@jwt_required()
def protected():
return '%s' % current_identity
if __name__ == '__main__':
app.run()
JWTの初期化
コードのこの部分で初期化しています。
jwt = JWT(app, authenticate, identity)
第1引数 (app)
アプリケーションを渡してあげます。
第2引数 (authentication_handler)
実際に認証を行う関数を渡します。
authentication_handlerは username
, password
の2つの引数を受け取り、
認証されたIdentityオブジェクトを返却する必要があります。
第3引数 (identity_handler)
current_identityで参照するIdentityを返却するようにします。
APIを叩いて認証してみる
サーバ起動
export FLASK_APP=app.py; flask run
リクエスト
curl http://localhost:5000/auth -X POST -H "Content-Type: application/json" -d '{ "username": "user1", "password": "abcxyz" }'
認証に成功すると、アクセストークンが返ってきます。
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NTE2MDM0NzUsImlhdCI6MTU1MTYwMzE3NSwibmJmIjoxNTUxNjAzMTc1LCJpZGVudGl0eSI6MX0.Cs351X-LvACQBQYnQVIYUh2hFvYDo3YEMI1Ll_Vyo2g"
}
エンドポイントに対して認証を要求する
@jwt_required()
というデコレータを使用して、
特定のエンドポイントに対して認証を要求することができます。
AuthorizationヘッダにJWT <アクセストークン>
の形でトークンを付与することで、
認証を行うことができます。
これはデフォルト設定ですが、プリフィックスはカスタマイズできます。
app.py の /protected
を叩いてみます。
curl http://localhost:5000/protected -H "Authorization: JWT <アクセストークン>"
User(id='1')
トークンの有効期限が切れているなど、認証に失敗した場合は以下のようなレスポンスが返ってきます。
ログイン後のユーザーのみ使用できるAPIなどは、
このようにエンドポイントにデコレータを付けるだけで実装できます。
{
"description": "Signature has expired",
"error": "Invalid token",
"status_code": 401
}
認証済みユーザーの情報を取得する
/protected
では、 レスポンスの内容としてcurrent_identityを返しています。
ここで返されるcurrent_identityの内容は def identity()
の返り値です。
@jwt_required()
が付与された関数内では、
current_identityを使用してトークンが渡されたユーザーにアクセスすることができます。
トークンに加えて権限チェックもしたい場合などは、
current_identityにroleも持たせておくなどすると良さそうです。
カスタマイズ
さきほど触れたAuthorizationヘッダのプリフィックスや、
authentication_handlerのキー(username, password)などはカスタマイズ可能です。
カスタマイズする場合は、config等で設定してあげるとよいと思います。
カスタマイズ可能な項目は、
ドキュメントのConfiguration Optionsに一覧で掲載されています。
おわりに
これまでフロントがメインで、バックエンドはまだ始めたばかりなので、
こうするともっといいよ、などのアドバイス等ありましたら
ご助言頂けますと幸いです。
よろしくお願いいたします。