6.1 サーバレスアーキテクチャと認証: LambdaでのJWT認証について #
サーバレスアーキテクチャでは伝統的なセッションベースの認証の代わりに、JSON Web Token(JWT)のような方法で認証をする必要があります。ここではその理由と解決策について解説します。
サーバレス環境とは何か #
まず最初に、サーバレス環境とは何かを理解することから始めます。サーバレスアーキテクチャ(FaaS、Function as a Serviceとも呼ばれます)では、開発者は個々の関数を書き、それらの関数がイベント(HTTPリクエストなど)に応じて自動的にスケーリングされ、実行されます。そのため、サーバの起動や維持について心配する必要がありません。
AWS Lambdaは、このようなサーバレスアーキテクチャの一例であり、開発者はコードのロジックに専念することができます。
サーバレスアーキテクチャでの認証 #
しかし、この種のアーキテクチャには、セッションベースの認証に関していくつかの課題があります。セッションベースの認証では、認証情報はサーバ内のセッションストアに保存され、そのセッションIDがクライアントに送り返されます。クライアントはその後、そのセッションIDを使用してさらなるリクエストを行います。
しかし、サーバレス環境では、リクエストごとに新しいコンテナ(またはランタイム環境)が作成され、リクエストの終了後に破棄されます。これは、サーバの状態を保存することが困難であることを意味し、それゆえにセッションベースの認証が動作しません。
JWT認証の利点 #
その代わりとなる方法として、JWT認証が推奨されます。JSON Web Token(JWT)は、クライアントとサーバ間で情報を安全に交換するためのコンパクトな方法です。これは、デジタル署名されたセキュアなトークンで、これによって認証や情報交換が可能になります。
JWTの利点は以下のとおりです:
ステートレス: JWTは自己完結型であり、トークン自体が認証情報を保持します。したがって、サーバはセッション情報を保持する必要がありません。この性質がサーバレスアーキテクチャと相性が良いのです。
スケーラビリティ: JWTはクライアントサイドで保存されるため、バックエンドサービスがステートレスであることを保証します。これにより、アプリケーションは容易にスケールアップすることができます。
セキュリティ: JWTはデジタル署名によって安全に保護されています。署名は秘密鍵を用いて行われるため、トークンの信頼性を確認することができます。
JWT認証のフロー #
以下に、JWT認証の基本的なフローを示します:
- ユーザーが認証情報(通常はユーザ名とパスワード)を用いてログインします。
- サーバは認証情報を確認し、そのユーザーのためのJWTを生成します。JWTには、ユーザーIDや有効期限などの情報(ペイロード)が含まれます。このトークンは、秘密鍵を用いて署名されます。
- JWTはクライアントに返され、クライアントはそれを保存します(通常はHTTP Only CookieやLocalStorageなどに)。
- クライアントは後続のリクエストにJWTを添付します(通常はHTTPヘッダのAuthorizationフィールド)。
- サーバは受け取ったJWTを検証し、そのペイロードからユーザー情報を取得します。
リクエストは認証され、レスポンスがクライアントに返されます。
flask-jwt-extendedを用いたJWT認証 #
FlaskでのJWT認証の基本的な実装は次のようになります。
from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key' # シークレットキーを設定します
jwt = JWTManager(app) # JWTManagerインスタンスを作成します
admin_user_id = 'your-user-id' # 管理者ユーザーID
admin_password = 'your-password' # 管理者パスワード
users = {admin_user_id: {"user_id": admin_user_id, "password": admin_password}}
# ログインページ
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user_id = request.form['user_id']
password = request.form['password']
user = users.get(user_id)
if not user or password != user['password']:
return "Invalid username or password", 401
# ログイン処理
access_token = create_access_token(identity=user_id)
return jsonify(access_token=access_token), 200
return render_template('login.html')
# 認証が必要なページ
@app.route('/api/protected-page', methods=['GET'])
@jwt_required() # このエンドポイントはJWTが必要です
def protected_page():
return jsonify({'auth': 'ok'}), 200
if __name__ == '__main__':
app.run()
このコードでは、/login
エンドポイントでユーザーIDとパスワードを受け取り、認証に成功した場合にはJWTを返します。/api/protected-page
エンドポイントでは、@jwt_required()
デコレータによってJWTが必要であることを示しています。
クライアントは、保護されたエンドポイント(この例では/api/protected-page)にアクセスするために、このトークンを使用します。このエンドポイントにアクセスするためには、HTTPヘッダのAuthorizationフィールドにBearer <アクセストークン>を設定する必要があります。このトークンが検証され、有効であると確認された場合にのみ
バックエンドでは以上のような処理が行われます、クライアント側ではどのような処理が行われるのでしょうか?
フロントエンドでのJWT認証を使用したログイン #
ユーザがログインする際に、Flaskバックエンドの/loginエンドポイントに対してPOSTリクエストを送ります。成功すると、このリクエストはJWTを含むレスポンスを返します。
このJWTをブラウザのlocalStorageに保存します。これにより、ユーザが再度サイトを訪れたときに、トークンを再度取得する必要がなくなります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>NanoCMS | ログイン</title>
<script>
var rootURL = "{{ url_for('index') }}";
function onSubmit(e) {
e.preventDefault();
let form = e.target;
let formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if(data.access_token) {
localStorage.setItem('access_token', data.access_token);
window.location.href = rootURL;
} else {
alert('ログインに失敗しました');
}
})
.catch((error) => {
console.error('Error:');
});
}
</script>
</head>
<body>
<h1>ログイン</h1>
<p>ログインしてください。</p>
<form action="{{ url_for('login') }}" method="post" onsubmit="onSubmit(event)">
<label for="user_id">ユーザーネーム</label>
<input type="text" name="user_id" id="user_id" placeholder="mailaddress@example.com" required>
<label for="password">パスワード</label>
<input type="password" name="password" id="password" placeholder="*********" required>
<button type="submit">ログイン</button>
</form>
</body>
</html>
保護されたページにアクセスした際に認証を走らせて、認証に失敗したらログインページにリダイレクトする #
base.htmlに以下のようなスクリプトを追加します。
window.onload = function() {
var jwt = localStorage.getItem('access_token');
var loginURL = "{{ url_for('login') }}";
var apiURL = "{{ url_for('protected_page') }}";
if (!jwt) {
console.log('No JWT found in localStorage');
// If there's no JWT in localStorage, redirect to the login page
window.location.href = loginURL;
} else {
// If there's a JWT, fetch the protected page content
fetch(apiURL, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + jwt,
},
})
.then(function(response) {
if (response.ok) {
return response.text();
} else {
localStorage.removeItem('access_token');
throw new Error('Failed to fetch protected page');
}
})
.catch(function(error) {
console.error('There has been a problem with your fetch operation:', error);
localStorage.removeItem('access_token');
});
}
};
ページを表示する際に、localStorageに保存されているJWTをAuthorizationヘッダに設定して送信します。 失敗したら、ログインページにリダイレクトします。 認証に失敗したらローカルストレージを削除しています。
ログアウト処理を追加する #
base.htmlに以下のようなスクリプトを追加します。
function logout() {
localStorage.removeItem('access_token');
var loginURL = "{{ url_for('login') }}";
window.location.href = loginURL;
}
まとめ #
この方法を用いることで、AWS Lambdaのようなサーバレス環境でも、セッションベースの認証をJWT認証に置き換えることができます。これにより、アプリケーションはスケーラビリティとセキュリティを確保しつつ、認証を効率的に行うことが可能になります。