はじめに

本記事では、ロングランテストを実施するために、負荷テストツールのApache JMeter(以下JMeter)でアクセストークン1 を更新する方法について解説します。

開発中のアプリについて、バックエンドにリクエストを数日間送り続けた時に性能要件を満たしているか調べる必要があり、ロングランテストを実施しました(性能テスト計画ガイド)。テスト実施のために、フロントエンドの代わりとして、バックエンドに自動でリクエストを送るJMeterのシナリオを作成しました。

このアプリではOAuth2.0を使用しており、認可サーバから取得したアクセストークンを使ってフロントエンドからのHTTPリクエストの認可制御を行っています。

アクセストークンには有効期限があるため、時間の経過により有効期限切れによるエラーが発生します。

アプリのフロントエンドでは、リフレッシュトークン2 を使ってアクセストークンを自動更新する仕組みを実装しエラーを解消できるようにしています。

しかし、現在JMeterにはアクセストークンの自動更新を行うプラグインが存在していません。そのため、シナリオ実行時に発生するアクセストークンの有効期限切れを解消するために、フロントエンドに実装されているアクセストークンを再取得・更新する実装をシナリオにも組み込む必要がありました。

リフレッシュトークンがあればアクセストークンの更新が可能

OAuth2.0の仕組み

OAuth2.0を使用したアプリでは、フロントエンドがアクセストークンを含んだHTTPリクエストをバックエンドに送り、バックエンドでアクセストークンの正当性を検証することで認可制御を行っています。

ただしアクセストークンには有効期限があるため、一定時間経過すると有効期限が切れてしまいフロントエンドからのHTTPリクエストがエラーとなります。

このエラーを解消させるために、フロントエンドでは取得したリフレッシュトークンを使用して認可サーバからアクセストークンを再取得する処理(アクセストークンの更新)が必要になります。

  1. ユーザが認可サーバの要求によってIDとパスワードを入力し、認可サーバがアクセストークンとリフレッシュトークンを発行する。発行されたトークンをフロントエンドが保存する。
  2. フロントエンドがアクセストークンを付与したHTTPリクエストを送信し、それを受信したバックエンドがアクセストークンを検証して認可チェックを行う。
  3. アクセストークンの有効期限が切れると認可チェックに失敗するが、フロントエンドはリフレッシュトークンを使って新しいアクセストークンとリフレッシュトークンを取得する。

この図の黄色で示した部分からリフレッシュトークンがあればアクセストークンを取得できることが分かります。

このことを応用し、JMeterのシナリオにリフレッシュトークンを渡しておき、そのリフレッシュトークンを使って定期的にアクセストークンを更新することによって、アクセストークンの有効期限切れを回避できます。

JMeterでアクセストークンを更新させるための実装方針

リフレッシュトークンで認可サーバからアクセストークンを取得する処理を以下のステップで実装します。

  1. CSVファイルからJMeterにリフレッシュトークンを読み込ませる
  2. リフレッシュトークンを使って認可サーバからアクセストークンを取得する
  3. 取得したアクセストークンを保存する
  4. アクセストークンの残り有効期間を計算し指定値未満ならトークン更新を実行する

実装の完成イメージは以下の図の通りです。

アクセストークン更新の実装

本記事の実装はJMeterバージョン5.6.3で動作することを確認しています。

本記事内のアクセストークンを取得するためのHTTPリクエストはAzure Active Directory B2C(以下Azure AD B2C)に対応したものになっています。

Azure AD B2C以外のサービスを利用している場合でも、OAuth2.0を使用したアプリであれば仕組みは同じであるため、HTTPリクエストの設定を修正することで利用できると考えています。

1. JMeterにリフレッシュトークンを読み込ませる

最初にJMeterにリフレッシュトークンを渡します。

あらかじめリフレッシュトークンを取得しておいてください。

取得したリフレッシュトークンを記載したCSVファイルを作成します。ここではファイル名をrefresh_token.csvとします。

以下が作成したCSVファイルの中身の例です。

eyJraWQiOiI3WUhjQ2ZMcU5NSjNlSVo0OUcyMTdheGJQMEo1Zl9kNTI4S0NZNFgx…

ここからが、JMeterシナリオの実装になります。

作成したrefresh_token.csvを読み込むために、シナリオにCSV Data Set Configを追加します。

以下のようにファイル名と変数名を指定してください。

// CSV Data Set Config
Filename: ./refresh_token.csv
Variable Names (comma-delimited): refresh_token

シナリオを実行すると、refresh_token.csvに記載されたリフレッシュトークンを読み込み、変数refresh_tokenに代入します。

2. 認可サーバからアクセストークンを取得する

変数refresh_tokenでアクセストークンを取得する処理を実装します。

リクエストのための設定はアクセストークンの更新処理について説明しているAzureの公式ドキュメントを参考に行いました。

以下のようにHTTPリクエストを送ることでトークンの更新ができると記載されています。

// HTTP
POST https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/token HTTP/1.1Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &client_id=90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6 &scope=90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6 offline_access &refresh_token=AwABAAAAvPM1KaPlrEqdFSBzjqfTGBCmLdgfSTLEMPGYuNHSUYBrq... &redirect_uri=urn:ietf:wg:oauth:2.0:oob

この情報を参考にHTTPリクエストをJMeterのシナリオ内で実装します。

このHTTPリクエストはパラメータが多く、ヘッダーにも設定が必要であるため、まずはPOSTMANのようなAPIを実行し結果を確認できるツールでアクセストークンを取得するために必要な設定を確認することをおすすめします。

シナリオにHTTP Request Samplerを追加し以下のように設定しました。

// HTTP Request Sampler
Protocol: https
Server name or IP: XXX.b2clogin.com
Request method: POST
Path: XXX.onmicrosoft.com/oauth2/v2.0/token?grant_type=refresh_token&refresh_token=${refresh_token}&client_id=[client_idを指定する]&p=[policyを指定する]

${refresh_token}としたところは、実行時に変数refresh_tokenが代入されるようにしています。

[client_idを指定する][policyを指定する]はご自身のAzure AD B2Cで指定されている値に置換してください。

 

ヘッダーはHTTP Header Managerを追加し以下のように設定しました。認可サーバにリクエストするために必要な情報を設定してください。

// HTTP Header Manager
Content-Type: application/x-www-form-urlencoded
Host XXX.b2clogin.com
Content-length: 0

このHTTPリクエストを実施した結果が下になります。認可サーバからアクセストークンとその有効期限を含むJSONがレスポンスされました。

// レスポンスされるJSONの例
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkhVclNUZkgwS1ZqUkR4cEduT2lhZ3...", // アクセストークン
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkhVclNUZkgwS1ZqUkR4cEduT2lhZ3Y0...",
"not_before": 1731324947,
"expires_in": 3600,
"expires_on": 1731328547, // アクセストークンの有効期限
"resource": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id_token_expires_in": 3600,
"profile_info": "eyJ2ZXIiOiIxLjAiLCJ0aWQiOiI1ZDcyZDdiOC1iZmQ1LTRlYzgtOTNlNC1hZjc3ZmRi...",
"scope": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx offline_access openid",
"refresh_token": "eyJraWQiOiI3WUhjQ2ZMcU5NSjNlSVo0OUcyMTdheGJQMEo1Zl9kNTI4S0NZNFgxZ1VZI...",
"refresh_token_expires_in": 1209600
}

3. アクセストークンを保存する

レスポンスからアクセストークンを抽出するために、作成したHTTP Request Samplerを右クリックしAdd PostProcessorからJSON Extractorを追加します。レスポンスされるJSONからキーが”access_token”の値を取得して変数access_tokenに代入するように設定します。

// JSON Extractor
Names of created variables: access_token
JSON Path expressions: $.access_token

この変数access_tokenを参照することで、HTTPリクエストを追加時にリクエストヘッダーにアクセストークンを付与することが可能になります。

ロングランテストでは作成したシナリオを何度もループさせるため、トークンの更新処理をシナリオの先頭に設置することでループのたびに新しいアクセストークンを取得し変数access_tokenに代入します。リフレッシュトークンはその有効期限内であれば複数回アクセストークンを取得することが可能です。

これによって、アクセストークンの有効期限切れを起こさずにバックエンドとの通信が可能になりました。

4. アクセストークンの残り有効期間によってトークン更新を実行する

4までの実装でアクセストークンの有効期限切れを防ぐことができるようになりました。

ただし、シナリオ1ループにかかる時間がアクセストークンの有効期間より短い場合、ループのたびに毎回トークン更新をする必要はありません。

このシナリオのままではループのたびに認可サーバにアクセスしてしまうため、認可サーバに想定より多いリクエストが発生するという実際のアプリ使用状況とは違った状態になってしまいます。

そこでトークンの更新頻度を落とすようにシナリオを修正します。取得したアクセストークンの有効期限をもとに指定した残り時間を満たしていれば更新処理は実行しないというロジックを実装しました。

具体的には、トークン更新時にそのトークンの有効期限を取得しておき、現在時刻との比較から残り有効時間を計算し、指定した時間(実装例では10分)未満ならトークン更新が実行されるようにします。

まずは、レスポンスからアクセストークンの有効期限を抽出するように設定します。HTTP Request Samplerを右クリックしAdd PostProcessorからJSON Extractorを追加します。

レスポンスされたJSONからキーが”expire_on”の値を取得して変数expire_onに代入するように設定します。

// JSON Extractor
Names of created variables: expire_on 
JSON Path expressions: $.expire_on

この変数expire_onを参照することで、アクセストークンの有効期限を確認できます。

JSR223 Samplerを作成し、現在時刻のタイムスタンプを変数timestampに代入します。

// JSR233 Sampler
Long timestamp = System.currentTimeMillis() / 1000;
vars.put("timestamp", timestamp.toString());

トークン更新処理をIf Controllerで囲み、実行条件を指定した残り時間以下になった場合とします。

以下では有効期限までの残り時間が10分未満になった場合にtrueが返され、トークン更新処理が実行されます。

// If Controller
${__groovy((Long.valueOf(Long.valueOf(vars.get("expires_on"))) - Long.valueOf(Long.valueOf(vars.get("timestamp"))))  < 10 * 60)}

初回のアクセストークン取得前は有効期限が設定されていない(NULL)ですが、有効期限を0に設定することでトークンリフレッシュが必ず実行されるようにしています。

// If Controller
${__groovy(vars.get("expires_on") == null)}

// JSR233 Sampler
vars.put("expires_on","0");

まとめ

JMeterのシナリオ内でアクセストークンを自動更新する方法を説明しました。

まとめると以下の手順で実装しました。

  1. CSVファイルからJMeterにリフレッシュトークンを読み込ませる
  2. リフレッシュトークンを使って認可サーバからアクセストークンを取得する
  3. 取得したアクセストークンを保存する
  4. アクセストークンの残り有効期間を計算し指定値未満ならトークン更新を実行する

    このように実装することで、アクセストークンの有効期限切れが発生することなくロングランテストを実施することが可能になります。

    本記事では対応しなかったこと

    リフレッシュトークンの更新

    トークン更新時にアクセストークンだけではなくリフレッシュトークンも取得できますが、リフレッシュトークンについてはJMeterシナリオ内で保持している値を更新しませんでした。

    リフレッシュトークンの更新処理を入れると、リフレッシュトークンの有効期限よりも長い期間シナリオを実行できるようになります。今回はリフレッシュトークンの有効期間内でシナリオを実行できたため組み込んでいません。

    認可サーバへのログイン操作の組み込み

    ログイン操作をJMeterシナリオ内で実行する実装は行いませんでした。

    この実装をすることでリフレッシュトークンを手動で取得するという準備作業が不要になります。

    脚注

    1. アクセストークンとは、API通信時にクライアントが認可されたことを示す資格情報のこと。
    2. リフレッシュトークンとは、アクセストークンを認可サーバから再取得する時にクライアントが認可されたことを示す資格情報のこと。アクセストークンよりも有効期間が長い。