Javaアプリケーションで安全な認証を構築する準備をしていますか、それとも苦労していますか? トークン(特にJSON Webトークン)を使用する利点、またはトークンをどのようにデプロイする必要があるかわからない場合は、 このチュートリアルでは、これらの質問などに回答できることをうれしく思います。

JSON Web Token( JWTs )と JJWTライブラリ(StormpathのCTOであるLes Hazlewoodによって作成され、寄稿者のコミュニティによって維持されている)に飛び込む前に、いくつかの基本をカバーしましょう。

1. 認証と トークン認証

アプリケーションがユーザーIDを確認するために使用する一連のプロトコルは認証です。 アプリケーションは従来、セッションCookieを介してIDを保持してきました。 このパラダイムは、セッションIDのサーバー側ストレージに依存しているため、開発者は、一意でサーバー固有のセッションストレージを作成するか、完全に別個のセッションストレージレイヤーとして実装する必要があります。

トークン認証は、サーバー側のセッションIDが解決できなかった問題を解決するために開発されました。 従来の認証と同様に、ユーザーは検証可能な資格情報を提示しますが、セッションIDの代わりにトークンのセットが発行されるようになりました。 初期のクレデンシャルは、標準のユーザー名とパスワードのペア、APIキー、または別のサービスからのトークンである可能性があります。 (StormpathのAPIキー認証機能はその一例です。)

1.1. なぜトークン?

非常に簡単に言えば、セッションIDの代わりにトークンを使用すると、サーバーの負荷を軽減し、権限管理を合理化し、分散型またはクラウドベースのインフラストラクチャをサポートするための優れたツールを提供できます。 JWTの場合、これは主にこれらのタイプのトークンのステートレスな性質によって実現されます(詳細は以下を参照)。

トークンは、クロスサイトリクエストフォージェリ( CSRF )保護スキーム、 OAuth 2.0 インタラクション、セッションID、および(Cookie内の)認証表現など、さまざまなアプリケーションを提供します。 ほとんどの場合、標準ではトークンの特定の形式は指定されていません。 HTML形式の一般的なSpringセキュリティCSRFトークンの例を次に示します。

<input name="_csrf" type="hidden" 
  value="f3f42ea9-3104-4d13-84c0-7bcb68202f16"/>

適切なCSRFトークンなしでそのフォームを投稿しようとすると、エラー応答が返されます。これがトークンの有用性です。 上記の例は「ダム」トークンです。 これは、トークン自体から収集される固有の意味がないことを意味します。 これは、JWTが大きな違いを生む場所でもあります。

2. JWTには何が含まれていますか?

JWT(「jots」と発音)は、さまざまなアプリケーションでトークンとして使用できる、URLセーフで、エンコードされ、暗号化された(暗号化された)文字列です。 CSRFトークンとして使用されているJWTの例を次に示します。

<input name="_csrf" type="hidden" 
  value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdCI6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9.rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc"/>

この場合、トークンが前の例よりもはるかに長いことがわかります。 前に見たように、フォームがトークンなしで送信されると、エラー応答が返されます。

では、なぜJWTなのですか?

上記のトークンは暗号で署名されているため、改ざんされていないことを証明するために検証できます。 また、JWTはさまざまな追加情報でエンコードされます。

JWTの構造を見て、このすべての長所をどのように絞り出すかをよりよく理解しましょう。 ピリオドで区切られた3つの異なるセクションがあることに気づいたかもしれません(.):

ヘッダ eyJhbGciOiJIUzI1NiJ9
ペイロード eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0
サイン rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

各セクションはbase64URLエンコードされています。 これにより、URLで安全に使用できるようになります(これについては後で詳しく説明します)。 各セクションを個別に詳しく見ていきましょう。

2.1. ヘッダー

ヘッダーをデコードするためにbase64を使用すると、次のJSON文字列が取得されます。

{"alg":"HS256"}

これは、JWTがSHA-256を使用してHMACで署名されたことを示しています。

2.2. ペイロード

ペイロードをデコードすると、次のJSON文字列が得られます(わかりやすくするためにフォーマットされています)。

{
  "jti": "e678f23347e3410db7e68767823b2d70",
  "iat": 1466633317,
  "nbf": 1466633317,
  "exp": 1466636917
}

ご覧のとおり、ペイロード内には、値を持つキーがいくつかあります。 これらのキーは「クレーム」と呼ばれ、 JWT仕様には、これらのうち7つが「登録済み」クレームとして指定されています。 彼らです:

iss 発行者
サブ 主題
aud 観客
exp 有効期限
nbf 以前ではない
iat で発行された
jti JWT ID

JWTを構築するときは、任意のカスタムクレームを入力できます。 上記のリストは、使用されるキーと予想されるタイプの両方で予約されているクレームを示しています。 CSRFには、JWT ID、「Issued At」時間、「Not Before」時間、および有効期限があります。 有効期限は、発行時刻のちょうど1分過ぎです。

2.3. 署名

最後に、署名セクションは、ヘッダーとペイロードを一緒に(。とともに)取得することによって作成されます。 間に)、既知のシークレットとともに指定されたアルゴリズム(この場合はSHA-256を使用するHMAC)を通過させます。 シークレットは常にバイト配列であり、使用するアルゴリズムに適した長さである必要があることに注意してください。 以下では、バイト配列に変換された(読みやすさのために)base64でエンコードされたランダムな文字列を使用します。

擬似コードでは次のようになります。

computeHMACSHA256(
    header + "." + payload, 
    base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

秘密を知っている限り、自分で署名を生成し、その結果をJWTの署名セクションと比較して、改ざんされていないことを確認できます。 技術的には、暗号で署名されたJWTはJWSと呼ばれます。 JWTは暗号化することもでき、JWEと呼ばれます。 (実際には、JWTという用語はJWEとJWSを表すために使用されます。)

これにより、CSRFトークンとしてJWTを使用するメリットに戻ります。 署名を検証し、JWTでエンコードされた情報を使用してその有効性を確認できます。 したがって、JWTの文字列表現は、サーバー側に保存されているものと一致する必要があるだけでなく、expクレームを調べるだけで期限切れにならないようにすることができます。 これにより、サーバーが追加の状態を維持する必要がなくなります。

さて、ここでは多くのことをカバーしました。 いくつかのコードに飛び込みましょう!

3. JJWTチュートリアルを設定する

JJWT( https://github.com/jwtk/jjwt )は、エンドツーエンドのJSONWebトークンの作成と検証を提供するJavaライブラリです。 永遠に無料でオープンソース(Apache License、バージョン2.0)であり、その複雑さのほとんどを隠すビルダーに焦点を合わせたインターフェースで設計されました。

JJWTを使用する際の主な操作には、JWTの構築と解析が含まれます。 次にこれらの操作を見てから、JJWTのいくつかの拡張機能について説明します。最後に、SpringセキュリティSpring BootアプリケーションでCSRFトークンとして動作しているJWTを確認します。

次のセクションで示されるコードは、ここにあります。 注:プロジェクトは、公開するAPIとのやり取りが簡単なため、最初からSpringBootを使用しています。

Spring Bootの優れた点の1つは、アプリケーションの構築と起動がいかに簡単かということです。 JJWT Funアプリケーションを実行するには、次のようにします。

mvn clean spring-boot:run

このサンプルアプリケーションでは、10個のエンドポイントが公開されています(私はhttpieを使用してアプリケーションと対話します。 ここにあります。)

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

  http http://localhost:8080/
	This usage message

  http http://localhost:8080/static-builder
	build JWT from hardcoded claims

  http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
	build JWT from passed in claims (using general claims map)

  http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
	build JWT from passed in claims (using specific claims methods)

  http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
	build DEFLATE compressed JWT from passed in claims

  http http://localhost:8080/parser?jwt=<jwt>
	Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=<jwt>
	Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

  http http://localhost:8080/get-secrets
	Show the signing keys currently in use.

  http http://localhost:8080/refresh-secrets
	Generate new signing keys and show them.

  http POST http://localhost:8080/set-secrets 
    HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
	Explicitly set secrets to use in the application.

次のセクションでは、これらの各エンドポイントと、ハンドラーに含まれるJJWTコードについて説明します。

4. JJWTを使用したJWTの構築

JJWTの流暢なインターフェースのため、JWTの作成は基本的に3ステップのプロセスです。

  1. 発行者、件名、有効期限、IDなどのトークンの内部クレームの定義。
  2. JWTの暗号化署名(JWSにします)。
  3. JWT Compact Serialization ルールに従って、JWTをURLセーフな文字列に圧縮します。

最終的なJWTは、指定された署名アルゴリズムで署名され、提供されたキーを使用する、base64でエンコードされた3つの部分からなる文字列になります。 この時点で、トークンを他のパーティと共有する準備が整います。

動作中のJJWTの例を次に示します。

String jws = Jwts.builder()
  .setIssuer("Stormpath")
  .setSubject("msilverman")
  .claim("name", "Micah Silverman")
  .claim("scope", "admins")
  // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
  .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
  // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
  .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
  .signWith(
    SignatureAlgorithm.HS256,
    TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
  )
  .compact();

これは、コードプロジェクトのStaticJWTController.fixedBuilderメソッドにあるコードと非常によく似ています。

この時点で、JWTと署名に関連するいくつかのアンチパターンについて説明する価値があります。 これまでにJWTの例を見たことがあれば、次の署名アンチパターンシナリオのいずれかに遭遇した可能性があります。

  1. .signWith(
        SignatureAlgorithm.HS256,
       "secret".getBytes("UTF-8")    
    )
  2. .signWith(
        SignatureAlgorithm.HS256,
        "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
    )
  3. .signWith(
        SignatureAlgorithm.HS512,
        TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
    )

HS型アノテーションアルゴリズムはいずれもバイト配列を取ります。 人間が文字列を読み取ってバイト配列に変換するのは便利です。

上記のアンチパターン1は、これを示しています。 これは、秘密が非常に短いために弱くなり、ネイティブ形式のバイト配列ではないため、問題があります。 したがって、読みやすくするために、バイト配列をbase64でエンコードできます。

ただし、上記のアンチパターン2は、base64でエンコードされた文字列を取得し、それを直接バイト配列に変換します。 実行する必要があるのは、base64文字列をデコードして元のバイト配列に戻すことです。

上記の3は、これを示しています。 では、なぜこれもアンチパターンなのですか? この場合、それは微妙な理由です。 シグニチャアルゴリズムがHS512であることに注意してください。 バイト配列は、 HS512 がサポートできる最大長ではないため、そのアルゴリズムで可能なものよりも弱い秘密になっています。

サンプルコードには、 SecretService というクラスが含まれており、特定のアルゴリズムに適切な強度のシークレットが使用されるようにします。 アプリケーションの起動時に、HSアルゴリズムごとに新しいシークレットのセットが作成されます。 シークレットを更新するだけでなく、シークレットを明示的に設定するエンドポイントがあります。

上記のようにプロジェクトを実行している場合は、以下を実行して、以下のJWTの例がプロジェクトからの応答と一致するようにします。

http POST localhost:8080/set-secrets \
  HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
  HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
  HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="

これで、 /static-builderエンドポイントに到達できます。

http http://localhost:8080/static-builder

これにより、次のようなJWTが生成されます。

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

今、ヒット:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

応答には、JWTを作成したときに含めたすべてのクレームが含まれています。

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

これは、次のセクションで説明する解析操作です。

それでは、クレームをパラメーターとして受け取り、カスタムJWTを構築するエンドポイントに到達しましょう。

http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true

hasMotorcycleクレームと他のクレームには微妙な違いがあります。 httpieは、JSONパラメーターがデフォルトで文字列であると想定しています。 httpieを使用して生のJSONを送信するには、 = ではなく:=フォームを使用します。 それがないと、「hasMotorcycle」:「true」が送信されますが、これは私たちが望んでいることではありません。

出力は次のとおりです。

POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jwt": 
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
    "status": "SUCCESS"
}

このエンドポイントをサポートするコードを見てみましょう。

@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map<String, Object> claims) 
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

2行目は、着信JSONが自動的にJavaマップに変換されることを保証します 、5行目のメソッドはそのマップを取得してすべてのクレームを一度に設定するため、JJWTにとって非常に便利です。

このコードは簡潔ですが、渡されたクレームが有効であることを確認するために、より具体的なものが必要です。 を使用して .setClaims(マップ請求) マップに表示されているクレームが有効であることがすでにわかっている場合は、この方法が便利です。 ここで、Javaの型安全性がJJWTライブラリに組み込まれます。

JWT仕様で定義されている登録済みクレームごとに、仕様が正しいタイプをとる対応するJavaメソッドがJJWTにあります。

この例で別のエンドポイントに到達して、何が起こるかを見てみましょう。

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true

「sub」クレームには整数5を渡したことに注意してください。 出力は次のとおりです。

POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "java.lang.ClassCastException",
    "message": "java.lang.Integer cannot be cast to java.lang.String",
    "status": "ERROR"
}

コードが登録済みクレームのタイプを適用しているため、エラー応答が返されます。 この場合、subは文字列である必要があります。 このエンドポイントをサポートするコードは次のとおりです。

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims) 
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();
    
    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                builder.setIssuer((String) value);
                break;
            case "sub":
                builder.setSubject((String) value);
                break;
            case "aud":
                builder.setAudience((String) value);
                break;
            case "exp":
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });
	
    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

前と同じように、メソッドは地図そのパラメータとしてのクレームの。 ただし、今回は、タイプを強制する登録済みクレームごとに特定のメソッドを呼び出しています。

これに対する1つの改良点は、エラーメッセージをより具体的にすることです。 現在、私たちの主張の1つが正しいタイプではないことを知っているだけです。 どの主張が誤りだったのか、それがどうあるべきかはわかりません。 より具体的なエラーメッセージを表示する方法を次に示します。 また、現在のコードのバグも扱います。

private void ensureType(String registeredClaim, Object value, Class expectedType) {
    boolean isCorrectType =
        expectedType.isInstance(value) ||
        expectedType == Long.class && value instanceof Integer;

    if (!isCorrectType) {
        String msg = "Expected type: " + expectedType.getCanonicalName() + 
		    " for registered claim: '" + registeredClaim + "', but got value: " + 
			value + " of type: " + value.getClass().getCanonicalName();
        throw new JwtException(msg);
    }
}

3行目は、渡された値が期待されるタイプであることを確認します。 そうでない場合は、JwtExceptionがスローされて特定のエラーが発生します。 以前に行ったのと同じ呼び出しを行って、これが実際に動作していることを見てみましょう。

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3

{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.JwtException",
    "message": 
      "Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
    "status": "ERROR"
}

これで、subクレームがエラーの1つであることを示す非常に具体的なエラーメッセージが表示されます。

コード内のそのバグに戻りましょう。 この問題は、JJWTライブラリとは何の関係もありません。 問題は、SpringBootに組み込まれているJSONからJavaへのオブジェクトマッパーが賢すぎて私たち自身の利益にはならないことです。

Javaオブジェクトを受け入れるメソッドがある場合、JSONマッパーは2,147,483,647以下の渡された数値をJava整数に自動的に変換します。 同様に、2,147,483,647を超える渡された数値をJava Longに自動的に変換します。 JWTのiat nbf 、および exp クレームの場合、マップされたオブジェクトが整数であるかLongであるかにかかわらず、ensureTypeテストに合格する必要があります。 そのため、渡された値が正しいタイプであるかどうかを判断するための追加の句があります。

 boolean isCorrectType =
     expectedType.isInstance(value) ||
     expectedType == Long.class && value instanceof Integer;

Longを期待しているが、値がIntegerのインスタンスである場合でも、それは正しいタイプであると言えます。 この検証で何が起こっているかを理解したので、これをdynamicBuilderSpecificメソッドに統合できます。

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims) 
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                ensureType(key, value, String.class);
                builder.setIssuer((String) value);
                break;
            case "sub":
                ensureType(key, value, String.class);
                builder.setSubject((String) value);
                break;
            case "aud":
                ensureType(key, value, String.class);
                builder.setAudience((String) value);
                break;
            case "exp":
                ensureType(key, value, Long.class);
                builder.setExpiration(Date.from(
				    Instant.ofEpochSecond(Long.parseLong(value.toString()))
				));
                break;
            case "nbf":
                ensureType(key, value, Long.class);
                builder.setNotBefore(Date.from(
					Instant.ofEpochSecond(Long.parseLong(value.toString()))
				));
                break;
            case "iat":
                ensureType(key, value, Long.class);
                builder.setIssuedAt(Date.from(
					Instant.ofEpochSecond(Long.parseLong(value.toString()))
				));
                break;
            case "jti":
                ensureType(key, value, String.class);
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

:このセクションのすべてのサンプルコードでは、JWTはSHA-256アルゴリズムを使用してHMACで署名されています。 これは、例を単純にするためです。 JJWTライブラリは、独自のコードで利用できる12の異なる署名アルゴリズムをサポートしています。

5. JJWTを使用したJWTの解析

前に、コード例にJWTを解析するためのエンドポイントがあることを確認しました。 このエンドポイントを打つ:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

この応答を生成します:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

StaticJWTControllerクラスのparserメソッドは次のようになります。

@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
    Jws<Claims> jws = Jwts.parser()
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

4行目は、着信文字列が署名付きJWT(JWS)であることを期待していることを示しています。 また、JWTの解析に署名するために使用されたのと同じシークレットを使用しています。 5行目は、JWTからのクレームを解析します。 内部的には、署名を検証しており、署名が無効な場合は例外をスローします。

この場合、キー自体ではなく、SignatureKeyResolverを渡していることに注意してください。 これは、JJWTの最も強力な側面の1つです。 JWTのヘッダーは、署名に使用されるアルゴリズムを示します。 ただし、JWTを信頼する前に検証する必要があります。 キャッチ22のようです。 SecretService.getSigningKeyResolverメソッドを見てみましょう。

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
    @Override
    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
        return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
    }
};

JwsHeader へのアクセスを使用して、アルゴリズムを検査し、JWTの署名に使用されたシークレットの適切なバイト配列を返すことができます。 ここで、JJWTは、このバイト配列をキーとして使用してJWTが改ざんされていないことを確認します。

渡されたJWTの最後の文字(署名の一部)を削除すると、次のようになります。

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

{
    "exceptionType": "io.jsonwebtoken.SignatureException",
    "message": 
      "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
    "status": "ERROR"
}

6. 実際のJWT:SpringSecurityCSRFトークン

この投稿の焦点はSpringSecurityではありませんが、ここで少し掘り下げて、JJWTライブラリの実際の使用法を紹介します。

クロスサイトリクエストフォージェリはセキュリティの脆弱性であり、悪意のあるWebサイトが、信頼を確立したWebサイトにリクエストを送信するように仕向けます。 これに対する一般的な解決策の1つは、シンクロナイザートークンパターンを実装することです。 このアプローチでは、トークンがWebフォームに挿入され、アプリケーションサーバーは受信トークンをリポジトリと照合して正しいことを確認します。 トークンが欠落しているか無効である場合、サーバーはエラーで応答します。

Spring Securityには、シンクロナイザートークンパターンが組み込まれています。 さらに良いことに、 Spring BootおよびThymeleafテンプレートを使用している場合は、シンクロナイザートークンが自動的に挿入されます。

デフォルトでは、SpringSecurityが使用するトークンは「ダム」トークンです。 それは単なる一連の文字と数字です。 このアプローチは問題なく機能します。 このセクションでは、JWTをトークンとして使用して、基本的な機能を拡張します。 送信されたトークンが期待されるものであることを確認することに加えて、JWTを検証して、トークンが改ざんされていないことをさらに証明し、有効期限が切れていないことを確認します。

まず、Java構成を使用してSpringSecurityを構成します。 デフォルトでは、すべてのパスに認証が必要であり、すべてのPOSTエンドポイントにCSRFトークンが必要です。 これまでに作成したものが引き続き機能するように、少しリラックスします。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private String[] ignoreCsrfAntMatchers = {
        "/dynamic-builder-compress",
        "/dynamic-builder-general",
        "/dynamic-builder-specific",
        "/set-secrets"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

ここでは2つのことを行っています。 まず、REST APIエンドポイントに投稿するときにCSRFトークンは不要であると言っています(15行目)。 次に、認証されていないアクセスをすべてのパスで許可する必要があると言っています(17〜18行目)。

Springセキュリティが期待どおりに機能していることを確認しましょう。 アプリを起動し、ブラウザで次のURLを押します。

http://localhost:8080/jwt-csrf-form

このビューのThymeleafテンプレートは次のとおりです。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!--/*/ <th:block th:include="fragments/head :: head"/> /*/-->
    </head>
    <body>
        <div class="container-fluid">
            <div class="row">
                <div class="box col-md-6 col-md-offset-3">
                    <p/>
                    <form method="post" th:action="@{/jwt-csrf-form}">
                        <input type="submit" class="btn btn-primary" value="Click Me!"/>
                    </form>
                </div>
            </div>
        </div>
    </body>
</html>

これは、送信時に同じエンドポイントにPOSTする非常に基本的なフォームです。 フォームにCSRFトークンへの明示的な参照がないことに注意してください。 ソースを表示すると、次のように表示されます。

<input type="hidden" name="_csrf" value="5f375db2-4f40-4e72-9907-a290507cb25e" />

これは、Spring Securityが機能していること、およびThymeleafテンプレートがCSRFトークンを自動的に挿入していることを知るために必要なすべての確認です。

値をJWTにするには、カスタムCsrfTokenRepositoryを有効にします。 Springセキュリティ構成の変更方法は次のとおりです。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CsrfTokenRepository jwtCsrfTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

これを接続するには、カスタムトークンリポジトリを返すBeanを公開する構成が必要です。 構成は次のとおりです。

@Configuration
public class CSRFConfig {

    @Autowired
    SecretService secretService;

    @Bean
    @ConditionalOnMissingBean
    public CsrfTokenRepository jwtCsrfTokenRepository() {
        return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
    }
}

そして、これが私たちのカスタムリポジトリ(重要な部分)です:

public class JWTCsrfTokenRepository implements CsrfTokenRepository {

    private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
    private byte[] secret;

    public JWTCsrfTokenRepository(byte[] secret) {
        this.secret = secret;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String id = UUID.randomUUID().toString().replace("-", "");

        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token;
        try {
            token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
            token = id;
        }

        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        ...
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        ...
    }
}

generateToken メソッドは、作成後30秒で有効期限が切れるJWTを作成します。 この配管が整ったら、アプリケーションを再度起動して、 /jwt-csrf-formのソースを確認できます。

これで、非表示フィールドは次のようになります。

<input type="hidden" name="_csrf" 
  value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZjIyMDdiNTVjOWM0MjU0YjZlMjY4MjQwYjIwNzZkMSIsImlhdCI6MTQ2NzA3MDQwMCwibmJmIjoxNDY3MDcwNDAwLCJleHAiOjE0NjcwNzA0MzB9.2kYLO0iMWUheAncXAzm0UdQC1xUC5I6RI_ShJ_74e5o" />

ハザ! これで、CSRFトークンはJWTになりました。 それはそれほど難しくありませんでした。

ただし、これはパズルの半分にすぎません。 デフォルトでは、SpringセキュリティはCSRFトークンを保存し、Webフォームで送信されたトークンが保存されたトークンと一致することを確認するだけです。 機能を拡張してJWTを検証し、有効期限が切れていないことを確認します。 そのために、フィルターを追加します。 Springセキュリティ構成は次のようになります。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }

    ...
}

9行目で、フィルターを追加し、デフォルトのCsrfFilterの後にフィルターチェーンに配置しています。 したがって、フィルターがヒットするまでに、JWTトークン(全体として)は、SpringSecurityによって保存された正しい値であることがすでに確認されています。

これがJwtCsrfValidatorFilterです(Springセキュリティ構成の内部クラスであるためプライベートです):

private class JwtCsrfValidatorFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
      HttpServletRequest request, 
      HttpServletResponse response, 
      FilterChain filterChain) throws ServletException, IOException {
        // NOTE: A real implementation should have a nonce cache so the token cannot be reused
        CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

        if (
            // only care if it's a POST
            "POST".equals(request.getMethod()) &&
            // ignore if the request path is in our list
            Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
            // make sure we have a token
            token != null
        ) {
            // CsrfFilter already made sure the token matched. 
            // Here, we'll make sure it's not expired
            try {
                Jwts.parser()
                    .setSigningKey(secret.getBytes("UTF-8"))
                    .parseClaimsJws(token.getToken());
            } catch (JwtException e) {
                // most likely an ExpiredJwtException, but this will handle any
                request.setAttribute("exception", e);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
                dispatcher.forward(request, response);
            }
        }

        filterChain.doFilter(request, response);
    }
}

23行目を見てください。 以前と同じようにJWTを解析しています。 この場合、例外がスローされると、リクエストはExpired-jwtテンプレートに転送されます。 JWTが検証すると、処理は通常どおり続行されます。

これにより、デフォルトのSpringSecurityCSRFトークンの動作をJWTトークンリポジトリとバリデーターでオーバーライドするループが閉じられます。

アプリを起動し、 / jwt-csrf-form を参照し、30秒以上待ってからボタンをクリックすると、次のように表示されます。

7. JJWT拡張機能

仕様を超えた機能のいくつかについて一言で、JJWTの旅を締めくくります。

7.1. クレームを強制する

解析プロセスの一部として、JJWTを使用すると、必要なクレームとそれらのクレームに必要な値を指定できます。 これは、JWTに有効であると見なすために存在しなければならない特定の情報がある場合に、非常に便利です。 クレームを手動で検証するための多くの分岐ロジックを回避します。 サンプルプロジェクトの/parser-enforceエンドポイントを提供するメソッドは次のとおりです。

@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt) 
  throws UnsupportedEncodingException {
    Jws<Claims> jws = Jwts.parser()
        .requireIssuer("Stormpath")
        .require("hasMotorcycle", true)
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

5行目と6行目は、登録済みクレームとカスタムクレームの構文を示しています。 この例では、issクレームが存在しないか、値Stormpathがない場合、JWTは無効と見なされます。 カスタムhasMotorcycleクレームが存在しないか、値がtrueでない場合も、無効になります。

まず、ハッピーパスに従うJWTを作成しましょう。

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jwt": 
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
    "status": "SUCCESS"
}

それでは、そのJWTを検証しましょう。

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http 
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
    },
    "status": "SUCCESS"
}

ここまでは順調ですね。 さて、今回は、hasMotorcycleを除外しましょう。

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman

今回、JWTを検証しようとすると:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc

我々が得る:

GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.MissingClaimException",
    "message": 
      "Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
    "status": "ERROR"
}

これは、hasMotorcycleクレームが予期されていたが、欠落していたことを示しています。

もう1つの例を見てみましょう。

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman

今回は、必要なクレームが存在しますが、値が間違っています。 次の出力を見てみましょう。

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http 
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.IncorrectClaimException",
    "message": "Expected hasMotorcycle claim to be: true, but was: false.",
    "status": "ERROR"
}

これは、hasMotorcycleクレームが存在したが、予期されていなかった値があったことを示しています。

MissingClaimExceptionIncorrectClaimExceptionは、JWTでクレームを適用するときの友達であり、JJWTライブラリだけが持つ機能です。

7.2. JWT圧縮

JWTに多くのクレームがある場合、JWTは大きくなる可能性があります。非常に大きくなるため、一部のブラウザーではGETURLに収まらない可能性があります。

大きなJWTを作成しましょう:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

生成するJWTは次のとおりです。

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

あの吸盤は大きい! それでは、同じ主張でわずかに異なるエンドポイントに到達しましょう。

http -v POST localhost:8080/dynamic-builder-compress \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

今回は、次のようになります。

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

62文字短くなりました! JWTの生成に使用されるメソッドのコードは次のとおりです。

@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map<String, Object> claims) 
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .compressWith(CompressionCodecs.DEFLATE)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

6行目で、使用する圧縮アルゴリズムを指定していることに注意してください。 これですべてです。

圧縮されたJWTの解析はどうですか? JJWTライブラリは圧縮を自動的に検出し、同じアルゴリズムを使用して解凍します。

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "and": "the",
            "brown": "fox",
            "dreamed": "of",
            "dreams": "you",
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "jumped": "over",
            "lazy": "dog",
            "rainbow": "way",
            "somewhere": "over",
            "sub": "msilverman",
            "the": "quick",
            "up": "high"
        },
        "header": {
            "alg": "HS256",
            "calg": "DEF"
        },
        "signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
    },
    "status": "SUCCESS"
}

ヘッダーのcalgクレームに注意してください。 これは自動的にJWTにエンコードされ、解凍に使用するアルゴリズムに関するヒントをパーサーに提供します。

注:JWE仕様は圧縮をサポートしています。 JJWTライブラリの今後のリリースでは、JWEと圧縮されたJWEをサポートします。 指定されていなくても、他のタイプのJWTでの圧縮を引き続きサポートします。

8. Java開発者向けのトークンツール

この記事の中心的な焦点はSpringBootまたはSpringSecurityではありませんでしたが、これら2つのテクノロジーを使用することで、この記事で説明したすべての機能を簡単に実証できました。 サーバーを起動して、これまでに説明したさまざまなエンドポイントを試してみることができるはずです。 ヒットするだけ:

http http://localhost:8080

Stormpath は、Javaコミュニティに多数のオープンソース開発者ツールを提供することにも興奮しています。 これらには以下が含まれます:

8.1. JJWT(私たちが話していること)

JJWT は、開発者がJavaでJWTを作成および検証するための使いやすいツールです。 Stormpathがサポートする多くのライブラリと同様に、JJWTは完全に無料でオープンソース(Apache License、バージョン2.0)であるため、誰もがその機能と方法を確認できます。 問題を報告したり、改善を提案したり、コードを送信したりすることを躊躇しないでください!

8.2. jsonwebtoken.ioおよびjava.jsonwebtoken.io

jsonwebtoken.io は、JWTのデコードを簡単にするために作成した開発者ツールです。 既存のJWTを適切なフィールドに貼り付けるだけで、ヘッダー、ペイロード、署名をデコードできます。 jsonwebtoken.ioは、Node.js開発者向けの最もクリーンな無料のオープンソース(Apache License、バージョン2.0)JWTライブラリであるnJWTを搭載しています。 このWebサイトでは、さまざまな言語用に生成されたコードを確認することもできます。 ウェブサイト自体はオープンソースであり、ここで見つけることができます。

java.jsonwebtoken.ioは特にJJWTライブラリ用です。 右上のボックスでヘッダーとペイロードを変更し、左上のボックスでJJWTによって生成されたJWTを確認し、下のボックスでビルダーとパーサーのJavaコードのサンプルを確認できます。 ウェブサイト自体はオープンソースであり、ここで見つけることができます。

8.3. JWTインスペクター

ブロックの新しい子供であるJWTInspector は、開発者がブラウザー内でJWTを直接検査およびデバッグできるようにするオープンソースのChrome拡張機能です。 JWTインスペクターは、サイト上のJWT(Cookie、ローカル/セッションストレージ、ヘッダー内)を検出し、ナビゲーションバーとDevToolsパネルから簡単にアクセスできるようにします。

9. JWT This Down!

JWTは、通常のトークンにインテリジェンスを追加します。 暗号で署名および検証し、有効期限を組み込み、その他の情報をJWTにエンコードする機能は、真にステートレスなセッション管理のステージを設定します。 これは、アプリケーションを拡張する機能に大きな影響を与えます。

Stormpathでは、OAuth2トークン、CSRFトークン、マイクロサービス間のアサーションなどにJWTを使用しています。

JWTの使用を開始すると、過去のダムトークンに戻ることはできません。 何か質問がある? Twitterの@afitnerdで私を襲ってください。