すべてのユーザーの認証済みリクエストをキャッシュする


9

同じコンテンツをリクエストするために、承認が必要な同時ユーザーの非常に大きなインパルスを処理する必要があるWebアプリに取り組んでいます。現在の状態では、32コアのAWSインスタンスでさえ完全に機能しなくなっています。

(Nginxをリバースプロキシとして使用していることに注意してください)

最悪の場合、JWTをデコードしてユーザーが認証されているかどうかを確認する必要があるため、応答を単純にキャッシュすることはできません。これは、ほとんどが同意するだろうLaravel 4、最大発射たち必要です遅い PHP-FPMとOpCacheが有効になっていても、。これは主に、ブートストラップ段階が多すぎるためです。

「これが問題になることがわかっているのに、なぜPHPとLaravelを最初に使用したのですか?」-しかし、その決定に戻るには今では遅すぎます!

可能な解決策

提唱されている1つの解決策は、LaravelからAuthモジュールを軽量の外部モジュール(Cなどの高速なもので記述)に抽出することです。このモジュールの責任は、JWTをデコードし、ユーザーが認証されるかどうかを決定することです。

リクエストの流れは次のようになります。

  1. キャッシュヒットかどうかを確認します(通常どおりPHPに渡されない場合)。
  2. トークンをデコードする
  3. 有効かどうかを確認する
  4. 場合は、有効な、キャッシュからサーブ
  5. 無効な場合は、Nginxに通知すると、NginxはリクエストをPHPに渡し、通常どおりに処理します。

これにより、このリクエストを1人のユーザーに提供した後はPHPにヒットせず、代わりに軽量モジュールに手を伸ばして、JWTのデコードやこのタイプの認証に伴うその他の警告をいじくります。

このコードを直接Nginx HTTP拡張モジュールとして書くことさえ考えていました。

懸念

私の懸念は、これがこれまでに行われたのを見たことがないことであり、より良い方法があるかどうか疑問に思いました。

また、ユーザー固有のコンテンツをページに追加すると、このメソッドは完全に強制終了されます。

Nginxで直接利用できる別の簡単なソリューションはありますか?または、ワニスのようなより専門的なものを使用する必要がありますか?

私の質問:

上記の解決策は意味がありますか?

これは通常どのように行われますか?

同様またはより良いパフォーマンスの向上を達成するためのより良い方法はありますか?


私は同様の問題に取り組んでいます。いくつかのアイデアa)Nginx auth_requestが認証マイクロサービスに引き渡すことができるため、Nginxモジュールを開発する必要性が軽減されます。b)または、マイクロサービスは、認証されたユーザーを、パブリックでキャッシュ可能で推測不可能な一時的なURLにリダイレクトできますが、PHPバックエンドによって検証され、一定の期間(キャッシュ期間)有効であることを確認できます。これにより、セキュリティがいくらか犠牲になります。一時的なURLが信頼できないユーザーに漏えいした場合、OAuthベアラートークンと同様に、その限られた期間コンテンツにアクセスできます。
James

これに対する解決策を思いつきましたか?私は同じことに直面しています
ティンブロダー

最適化されたバックエンドノードの大規模なクラスターを作成することで、負荷に対処できたことがわかりましたが、このアプローチは長期的には大きなコスト削減ソリューションであると確信しています。事前に提供できる応答の一部がわかっている場合、リクエストの流入前にキャッシュをウォームすると、バックエンドリソースの節約と信頼性の向上が非常に高くなります。
iamyojimbo 16

回答:


9

私は同様の問題に取り組んでいます。ユーザーは、すべてのリクエストに対して認証を受ける必要があります。私はユーザーをバックエンドアプリで少なくとも1回認証すること(JWTトークンの検証)に焦点を当ててきましたが、その後、バックエンドはもう必要ないと判断しました。

私は、デフォルトで含まれていないNginxプラグインを必要としないようにしました。それ以外の場合は、nginx-jwtまたはLuaスクリプトをチェックできます。これらはおそらく優れたソリューションです。

アドレッシング認証

これまでのところ、私は次のことを行いました:

  • を使用して認証をNginxに委任しましたauth_request。これは、internalリクエストをバックエンドトークン検証エンドポイントに渡す場所を呼び出します。これだけでは、多数の検証を処理する問題にまったく対処できません。

  • トークン検証の結果は、proxy_cache_key "$cookie_token";ディレクティブを使用してキャッシュされます。トークンの検証が成功すると、バックエンドは、Cache-Control最大5分間だけトークンをキャッシュするようにNginxに指示するディレクティブを追加します。この時点で、一度検証された認証トークンはキャッシュにあります。同じユーザー/トークンからの後続のリクエストは、認証バックエンドに影響しなくなります。

  • 無効なトークンによる潜在的なフラッディングからバックエンドアプリを保護するために、バックエンドエンドポイントが401を返したときに拒否された検証もキャッシュします。これらは、このような要求でNginxキャッシュがいっぱいになる可能性を回避するために、短時間だけキャッシュされます。

401(これもNginxによってキャッシュされます)を返すことでトークンを無効にするログアウトエンドポイントなど、いくつかの追加の改善点を追加しました。これにより、ユーザーがログアウトをクリックした場合、期限が切れていなくてもトークンを使用できなくなります。

また、私のNginxキャッシュには、すべてのトークンについて、関連付けられたユーザーがJSONオブジェクトとして含まれているため、この情報が必要な場合にDBから取得する必要がありません。また、トークンを復号化する必要もありません。

トークンの有効期間と更新トークンについて

5分後、トークンはキャッシュ内で期限切れになるため、バックエンドに再度クエリが実行されます。これは、ユーザーがログアウトしたり、侵害されたりしたため、トークンを無効化できるようにするためです。このような定期的な再検証と、バックエンドでの適切な実装により、リフレッシュトークンを使用する必要がなくなります。

従来、更新トークンは新しいアクセストークンを要求するために使用されていました。それらはバックエンドに保存され、アクセストークンのリクエストが、この特定のユーザーのデータベースにあるものと一致する更新トークンで行われることを確認します。ユーザーがログアウトした場合、またはトークンが危険にさらされた場合は、DB内の更新トークンを削除または無効化して、無効化された更新トークンを使用する新しいトークンの次のリクエストが失敗するようにします。

つまり、更新トークンは通常長い有効期間を持ち、常にバックエンドに対してチェックされます。これらは、有効期間が非常に短い(数分)アクセストークンを生成するために使用されます。これらのアクセストークンは通常、バックエンドに到達しますが、署名と有効期限のみを確認します。

ここでの設定では、アクセストークンと更新トークンの両方と同じ役割と機能を持つ、有効期間がより長い(数時間または1日の可能性がある)トークンを使用しています。検証と無効化はNginxによってキャッシュされているため、5分ごとに1回だけバックエンドによって完全に検証されます。したがって、複雑さを増すことなく、更新トークンを使用する(トークンをすばやく無効化できる)利点を維持します。また、署名と有効期限の確認のみに使用されている場合でも、単純な検証がNginxキャッシュよりも少なくとも1桁遅いバックエンドに到達することは決してありません。

この設定では、すべての着信リクエストがauth_requestNginxディレクティブに到達する前にそれに到達するため、バックエンドで認証を無効にすることができます。

リソースごとの承認を実行する必要がある場合、問題は完全には解決しませんが、少なくとも基本的な承認部分は保存しました。また、Nginxのキャッシュされた認証応答にデータが含まれ、それをバックエンドに返すことができるため、トークンの復号化を回避したり、DBルックアップを行ってトークンデータにアクセスしたりすることもできます。

今、私の最大の懸念は、セキュリティに関連する明らかな何かを、それを実現せずに壊してしまう可能性があることです。つまり、受信したトークンは、Nginxによってキャッシュされる前に、少なくとも1回は検証されます。強化されたトークンはすべて異なり、キャッシュキーも異なるため、キャッシュにヒットしません。

また、実際の認証では、追加のナンスまたは何かを生成(および検証)することで、トークン盗用と戦うことにも言及する価値があります。

これが私のアプリのNginx設定の簡単な抜粋です。

# Cache for internal auth checks
proxy_cache_path /usr/local/var/nginx/cache/auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=10m use_temp_path=off;
# Cache for content
proxy_cache_path /usr/local/var/nginx/cache/resx levels=1:2 keys_zone=content_cache:16m max_size=128m inactive=5m use_temp_path=off;
server {
    listen 443 ssl http2;
    server_name ........;

    include /usr/local/etc/nginx/include-auth-internal.conf;

    location /api/v1 {
        # Auth magic happens here
        auth_request         /auth;
        auth_request_set     $user $upstream_http_X_User_Id;
        auth_request_set     $customer $upstream_http_X_Customer_Id;
        auth_request_set     $permissions $upstream_http_X_Permissions;

        # The backend app, once Nginx has performed internal auth.
        proxy_pass           http://127.0.0.1:5000;
        proxy_set_header     X-User-Id $user;
        proxy_set_header     X-Customer-Id $customer;
        proxy_set_header     X-Permissions $permissions;

        # Cache content
        proxy_cache          content_cache;
        proxy_cache_key      "$request_method-$request_uri";
    }
    location /api/v1/Logout {
        auth_request         /auth/logout;
    }

}

次に、/auth上記のように含まれている内部エンドポイントの構成抽出を次に示します/usr/local/etc/nginx/include-auth-internal.conf

# Called before every request to backend
location = /auth {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_methods     GET HEAD POST;
    proxy_cache_key         "$cookie_token";
    # Valid tokens cache duration is set by backend returning a properly set Cache-Control header
    # Invalid tokens are shortly cached to protect backend but not flood Nginx cache
    proxy_cache_valid       401 30s;
    # Valid tokens are cached for 5 minutes so we can get the backend to re-validate them from time to time
    proxy_cache_valid       200 5m;
    proxy_pass              http://127.0.0.1:1234/auth/_Internal;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
    proxy_set_header        Accept application/json;
}

# To invalidate a not expired token, use a specific backend endpoint.
# Then we cache the token invalid/401 response itself.
location = /auth/logout {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_key         "$cookie_token";
    # Proper caching duration (> token expire date) set by backend, which will override below default duration
    proxy_cache_valid       401 30m;
    # A Logout requests forces a cache refresh in order to store a 401 where there was previously a valid authorization
    proxy_cache_bypass      1;

    # This backend endpoint always returns 401, with a cache header set to the expire date of the token
    proxy_pass              http://127.0.0.1:1234/auth/_Internal/Logout;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
}

コンテンツ配信への対応

これで、認証がデータから分離されました。すべてのユーザーで同一であると言ったので、コンテンツ自体もNginx(私の例ではcontent_cacheゾーン)でキャッシュできます。

スケーラビリティ

このシナリオは、Nginxサーバーが1台あると想定して、すぐに機能します。実際のシナリオでは、おそらく高可用性、つまり複数のNginxインスタンスがあり、(Laravel)バックエンドアプリケーションをホストしている可能性もあります。その場合、ユーザーが行うすべてのリクエストは任意のNginxサーバーに送信される可能性があり、すべてのトークンがローカルにキャッシュされるまで、ユーザーはバックエンドに到達してトークンを確認し続けます。サーバーの数が少ない場合でも、このソリューションを使用すると大きなメリットが得られます。

ただし、複数のNginxサーバー(およびキャッシュ)を使用すると、次のようにすべてのトークンキャッシュを(強制的に更新することにより)パージできないため、サーバー側でログアウトできなくなることに注意することが重要です。/auth/logout私の例ではそうします。5分間のトークンキャッシュ期間のみが残り、バックエンドへのクエリがすぐに強制され、リクエストが拒否されたことがNginxに通知されます。部分的な回避策は、ログアウト時にクライアントのトークンヘッダーまたはCookieを削除することです。

どんなコメントでも大歓迎です!


あなたはもっと多くの賛成票を得るべきです!とても助かります、ありがとう!
Gershon Papi

「私は、401(Nginxによってもキャッシュされる)を返すことによってトークンを無効にするログアウトエンドポイントなど、いくつかの追加の改善点を追加しました。これにより、ユーザーがログアウトをクリックした場合、トークンが期限切れになっていなくても使用できなくなります。 」-これは賢いです!、しかし実際にバックエンドでトークンをブラックリストに登録しているので、キャッシュがダウンしたり何かが発生した場合でも、ユーザーはまだその特定のトークンでログインできませんか?
gaurav5430

「ただし、複数のNginxサーバー(およびキャッシュ)では、すべてのトークンキャッシュを(強制的に更新することによって)パージできないため、サーバー側でログアウトできなくなることに注意することが重要です。 / auth / logoutが私の例で行うように。」詳しく説明できますか?
gaurav5430
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.