paint-brush
JWT がエンタープライズ アプリのセキュリティ保護の鍵となる理由 – そして KumuluzEE があなたの新しい親友になる理由@jesperancinha
新しい歴史

JWT がエンタープライズ アプリのセキュリティ保護の鍵となる理由 – そして KumuluzEE があなたの新しい親友になる理由

João Esperancinha40m2025/01/17
Read on Terminal Reader

長すぎる; 読むには

「JWT」または JavaScript Object Notation Web Token は、[RFC7519] で定義されている標準です。これは複数の方法で定義でき、2 者間で情報を転送するために使用できます。この記事では、一般的な Java エンタープライズ アプリケーションに `JWT` を統合する方法について説明します。
featured image - JWT がエンタープライズ アプリのセキュリティ保護の鍵となる理由 – そして KumuluzEE があなたの新しい親友になる理由
João Esperancinha HackerNoon profile picture
0-item

最近では、パフォーマンスに関する懸念がますます高まっており、同時に、システムが高速かつ確実に通信できる方法を知りたいと考えています。多くの場合、情報を送信し、可能な限り機密性と安全性を維持したいと考えています。機密データは、Web を介して公開され、ネットワークの反対側でアクションをトリガーする必要もあります。ほとんどの場合、データの変更を引き起こすアクションを生成したいと考えます。これらの場合、データの保護だけを検討しているわけではありません。データの送信によってトリガーされるアクションが信頼できるものであることを確認する必要があります。データを保護する方法はいくつかあります。最も一般的な方法は、 TLS (トランスポート層セキュリティ) の安全な接続を介してデータを送信します。これにより、データがネットワーク経由で暗号化されることが保証されます。証明書を使用して、2 者間の信頼関係を構築し、これを実現します。この記事では、 JWT標準について説明し、さらに、 JWT一般的なEnterpriseアプリケーションに統合する方法を説明します。この場合、 KumuluzEEについて説明します。いくつかの基本的な概念を見てみましょう。 JWTまたは JSON Web Token、または JavaScript Object Notation Web Token は、 RFC7519で定義されている標準です。この標準は、すべてのRFC (Request For Comments) 標準と同様に、 IETF (Internet Engineering Task Force) によって定義、作成、公開されています。定義方法は複数あります。一般的に、 JWT 2 者間でクレームを送信するためのコンパクトで安全な形式であると言えます。クレームを簡素化する 1 つの方法は、基本的に、情報を含む名前と値のペアとして説明することです。この情報は、インターネット通信のいくつかの重要な側面を保証するために必要です。まず、受信した情報が検証され、信頼されていることを確認する必要があります。次に、それを検証する必要があります。基本的にはこれだけです。この標準を実装するには、Java エンタープライズ アプリケーションの実装に役立ついくつかのフレームワークを使用できます。Spring Boot は広く使用されています。多くの場合、銀行やその他の金融機関などの特定の組織の独自のソフトウェアで別の名前でラップされています。この例では、何か違うことをすることにしました。 Spring Boot の代わりに、 KumuluzEEの例を見てみましょう。ポイントは、 JWTが何であるか、そしてそれがどのように見えるかを正確に特定することです。 Java エンタープライズ アプリケーションは基本的に、アプリケーション サーバーにデプロイすることも、組み込みサーバーを使用して単独で実行することもできるアプリケーションです。 たとえば、Spring Boot アプリケーションは組み込みの Tomcat サーバーで実行されます。 この記事では、 KumuluzEEに焦点を当てます。 Spring Boot と同様に、 KumuluzEE にも組み込みサーバーが含まれています。 ただし、この場合は Jetty と呼ばれます。 これは、CDI (コンテキスト依存性注入) を提供するために Weld と組み合わせて使用されます。 すべての Java EEおよびJakarta EEテクノロジ標準は、このframeworkと互換性があります。

2. 事例


JWT基本的な形でどのように機能するかを説明するために、私はそれを提示する方法を考えなければなりませんでした。セキュリティが懸念される典型的な例は銀行です。しかし、 JWTどのように機能するかを示すために銀行アプリケーション全体を作成するのは時間の無駄であり、おそらくあまりにも多くの概念が関係するでしょう。代わりに、私が作ったのは非常にシンプルな銀行システムです。私たちの主な関心事は、データがどのようにネットワークを流れるか、そしてユーザーがアプリケーションの特定の領域にどのようにアクセスするかを示すことです。また、TLS や暗号化された情報をネットワーク経由で送信する方法についても説明しません。最も純粋な形のJWTに焦点を当てます。私たちのケースは、自然と環境を保護するグループが使用する銀行システムです。これは、 JWTどのように機能するかを示す楽しい方法です。この自然連盟の主人公は、私のすべての記事で共通のキャラクターになりつつあるルーシーです。

3. 建築

始める前に、実行中のアプリケーションをスケッチしてみましょう。非常に単純なアプリケーションですが、それでも描いておくとよいでしょう。

これがとても簡単なのは、 JWTがすべてのリクエストでチェックされ、すべてのリクエストが公開鍵に対して検証されるため、すべてのリクエストで正しいトークンを送信すれば通過できることがわかっているからです。JWT JWT 、OAuth2、Okta SSO、またはその他の認証メカニズムと統合できます。この場合、認証と承認を確立しています。アプリケーションでは、 JWT使用し、署名を使用してメッセージを認証します。ただし、アプリケーションにログインすることはありません。代わりに、認証が成功した後にユーザーがアプリケーションを使用できるようにします。この時点で、 JWTの核は実際には完全なアプリケーションのごく一部であることが簡単にわかります。それでも、いくつかの機能を追加する必要があります。必要なリソースは次のとおりです。

  • バランスシステム
  • クレジットシステム


基本的なシステムでは、お金とクレジットのリクエストのみを登録するとしましょう。基本的には、価値を蓄積するだけです。また、クレジットを取得できる人もいれば、できない人もいます。お金を保管できる人もいれば、クレジットを取得できる人もいます。

4. テクノロジーの選択

はじめに述べたように、 KumuluzEEエンタープライズ アプリケーション フレームワークとして使用し、基本的なJWT用語と概念を確認できるような超基本的なアプリケーションを実装します。正しい Java バージョンを使用していることを確認してください。この段階では、最小限の Java 17 SDK がインストールされている必要があります。Maven、Git、IntelliJ などの Java 互換 IDE、および何らかのシェルが必要になります。

5. セットアップ

アプリケーションを開始するには、いくつかのKumuluzEE依存関係が必要です。これは主に、Spring Boot と同様に、 KumuluzEEにもいくつかの依存関係が必要であるためです。POM ファイルを簡単に見てみましょう。

 <dependencies> <dependency> <groupId>com.kumuluz.ee.openapi</groupId> <artifactId>kumuluzee-openapi-mp</artifactId> </dependency> <dependency> <groupId>com.kumuluz.ee.openapi</groupId> <artifactId>kumuluzee-openapi-mp-ui</artifactId> </dependency> <dependency> <groupId>com.kumuluz.ee</groupId> <artifactId>kumuluzee-microProfile-3.3</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.mockk</groupId> <artifactId>mockk-jvm</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.ninja-squad</groupId> <artifactId>springmockk</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.kotest</groupId> <artifactId>kotest-assertions-core-jvm</artifactId> <scope>test</scope> </dependency> </dependencies>

いくつかの依存関係について簡単に説明しましょう。これを読むときは、 pom.xmlファイルを上から下まで追ってください。これは、以下の説明を理解するために重要です。アプリケーションを動作させるには、依存関係のパッケージが必要です。幸いなことに、 KumuluzEE 、このアプリケーションを開始するための基本的な標準バンドルを含む Microprofile ライブラリを提供しています。これらはすべて、 KumuluzEE -Microprofile ライブラリに含まれています。必要なすべてのJWTパラメーターを使用してアプリを構成できるようにするには、MicroProfile ライブラリを追加する必要があります。同時に、JSON 処理ライブラリが必要です。これは Johnson Core が行うことです。もちろん、動作するにはKumuluzEEのコアが必要です。 Jetty は、 KumuluzEEフレームワークを実行する基盤となるサーバーです。これが、依存関係に Jetty が必要な理由です。 CDI必要であることを考えると、それをサポートするライブラリも必要です。 REST エンドポイントを有効にするには、 KumuluzEEの REST ライブラリが必要です。 API を取得するには、Geronimo ライブラリが必要です。これにより、 JSR-374の実装が利用可能になります。また、 JWTとそのJSON-formattedコンテンツを解釈する必要があります。Lombok は、それ自体は必要ありません。すべてが美しく輝くようになるだけです。ログをより適切に解釈して結果を理解するために、Logback も重要です。では、 resourcesフォルダーを見てみましょう。まず、このフォルダーに何があるかを理解しましょう。JWT やJWTに関連するものでアプリケーションを構成する必要があり、最後に、作成する Bean について何かを指定する必要があります。そこで最も単純なファイルを見てみましょう。beans.xml は META-INF にあります。

 <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" xmlns:weld="http://jboss.org/schema/weld/beans" bean-discovery-mode="all"> <weld:scan> <weld:exclude name="org.jesperancinha.fintech.model.Accounts"/> </weld:scan> </beans>

これは典型的なファイルであり、少し古いファイルだと思うかもしれません。この時点では、 KumuluzEE実行することだけが目的です。除外アクションがあります。これは、Weld に、Bean アクションのスキャンで Accounts クラスを考慮しないように指示します。これは重要です。使用している実装では、 Weld基本的に空のコンストラクタを持つすべてのクラスを Bean と見なすからです。Accounts を Bean と見なさない理由は後で説明します。今のところ、リクエストは Request スコープで作成していることに留意してください。これは、リクエストごとに異なるユーザーを持つことができるため、論理的です。では、「 logback 」がどのように実装されているかを見てみましょう。これはMETA-INFにも記載されています。

 <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> </root> </configuration>

これは、 logsの非常に簡単な構成です。最後に、おそらくアプリケーションで最も重要なファイルです。これは config-template です。この時点で、このプロジェクトで作成したファイルの一部がテンプレート構造の一部であることに注意することが重要です。これについては後で詳しく説明します。このテンプレート ファイルは、MicroProfile によって読み取られる config.yml ファイルに変換されるはずです。このファイルは、リソースのルートにあります。

 kumuluzee: name: your-financeje-banking version: 1.0.0 jwt-auth: public-key: {{ publicKey }} issuer: {{ issuer }} healthy: true

これらのプロパティが実際に何を意味するのかは、後で説明します。すべて説明不要です。publicKey と issuer はすべて置換されるパラメーターです。これについては後で説明します。bash スクリプトによって置換が確実に行われます。コーディングの準備はほぼ整いましたが、まずはJWTトークンの構造を見てみましょう。

6. 実践的なコード

非常に小さなアプリケーションを作成しましょう。このセクションでは、アプリケーションをJWTで動作させる方法について説明します。確認したいのは、一部のRESTメソッドにアクセスできるようにユーザーを指定し、他のメソッドにはアクセスできないようにできるかどうかです。このコードを確認する方法の 1 つは、まずプレーンなJWTトークンを確認することです。これが管理者の例です。

 { "iss": "joaofilipesabinoesperancinha", "jti": "01MASTERFINANCE", "sub": "admin", "aud": "nature", "upn": "admin", "groups": [ "user", "admin", "client", "credit" ], "user_id": 1, "access": "TOP", "name": "Admin" }

JSON内のこれらの名前はそれぞれクレームと呼ばれます。この例では、いくつかの予約済みクレームが表示されています。

  • iss 」 — これはトークンの発行者です。この値は任意に選択できます。このパラメータの値は、前に見た config.yml で置き換えられる発行者変数と一致する必要があります。
  • jti 」 — これはトークンの一意の識別子です。たとえば、このクレームを使用して、トークンが 2 回以上使用されるのを防ぐことができます。
  • sub 」 — これはトークンのサブジェクトです。ユーザーまたは任意のものにすることができます。これは識別子、キー、名前、または任意のものとしても使用できることを覚えておくことが重要です。
  • upn 」 — ユーザー プリンシパル名。これは、ユーザーが使用しているプリンシパルを識別するために使用されます。
  • groups 」 — これは現在のユーザーが属するグループの配列です。基本的に、これはこのトークンを使用したリクエストが実行できることを決定します。トークンには、いくつかのカスタムクレームがあります。これは予約済みクレームと同様に使用できます。
  • user_id 」 — これを使用してユーザー ID を設定します。
  • access 」 — ユーザーのアクセス レベルを決定します。
  • name 」 — ユーザーの名前。

7. 実践的なコード

これまでの内容をまとめてみましょう。決定した構造を持つトークンと通信することがわかっています。さらに、アプリケーションの構成、ログバック構成を設定し、最後にエンタープライズ Bean ルックアップのカスタム構成を設定しました。パッケージ モデルを見てみましょう。ここには 3 つのクラスがあります。これらのクラスは基本的に、アカウントの集約と、 clientaccount間の表現を表しています。このように、まず、 Clientが配置されている Kotlin ファイル Model.kt を見てみましょう。

 data class Client constructor( @JsonProperty var name: String ?= null )

この最初のモデル クラスは、クライアントの表現です。このケースのclientには名前しかありません。これは、" jwt " 属性名で表されるユーザー名です。さらに、 Accountあります。

 data class Account( @JsonProperty val accountNumber: String?, @JsonProperty val client: Client? = null, @JsonProperty var currentValue: BigDecimal = BigDecimal.ZERO, @JsonProperty var creditValue: BigDecimal = BigDecimal.ZERO ) { fun addCurrentValue(value: Long) = Account( accountNumber, client, currentValue .add(BigDecimal.valueOf(value)), creditValue ) fun addCreditValue(value: Long): Account = Account( accountNumber, client, currentValue, currentValue .add(BigDecimal.valueOf(value)) ) }


このクラスでは、基本的に accountNumber、client、currentValue、そして最後に creditValue を設定します。すべての値を 0 にデフォルト設定していることに注意してください。また、BigDecimal も使用していますが、これは単にお金を扱っているからです。お金は正確である必要があり、システムの切り上げや切り捨ての影響を受けることはありません。言い換えると、たとえば 0. 0000000000000000000000000000000000000000000000000001ユーロなどの数値は常にその数値のままである必要があります。また、アカウントに値を追加する必要があります。ここで、addCurrentValue メソッドが登場します。同じ理由で、 addCreditValueを使用してクレジットも補充します。最後に、データ設定の最後の部分で、 Accountsクラスを使用します。

 open class Accounts constructor( open val accountMap: MutableMap<String, Account> = mutableMapOf() )

これは本質的にはすべてのアカウントの集約です。マップ コンテンツを使用して、データベースの動作を模倣します。次に、コントローラー パッケージを見てみましょう。ここで、データ モデルを使用して実行するアプリケーションを作成します。まず、 BankApplicationクラスを見てみましょう。

 @LoginConfig(authMethod = "MP-JWT") @ApplicationPath("/") @DeclareRoles("admin", "creditor", "client", "user") class BankApplication : Application()


ここで、3 つの重要な点を述べます。LoginConfig アノテーションでは、MicroProfile に従ってJWTトークンを使用および理解するように定義します。ApplicationPath は、アプリケーション ルートを定義します。ここからアプリケーションの URL が始まります。この例では、 HTTP://localhost:8080になります。最後に、 DeclareRoles は、アプリケーションで使用および受け入れられるロールを定義します。この状況では、ロールとグループは互換性のある用語です。インジェクションを効率的に機能させるために、アカウント マップを識別するためのアノテーションを作成します。

 annotation class AccountsProduct

フルスクリーンモードに入る フルスクリーンモードを終了する

次に、キャッシュ オブジェクト ファクトリ AccountsFactory を作成します。

 class AccountsFactory : Serializable { @Produces @AccountsProduct @ApplicationScoped fun accounts(): Accounts = Accounts(mutableMapOf()) companion object { @Throws(JsonProcessingException::class) fun createResponse( currentAccount: Account, name: JsonString, accounts: Accounts, log: Logger, objectMapper: ObjectMapper, principal: Principal?, jsonWebToken: JsonWebToken? ): Response { val jsonObject = Json.createObjectBuilder() .add("balance", currentAccount.currentValue) .add("client", name) .build() accounts.accountMap[name.string] = currentAccount log.info("Principal: {}", objectMapper.writeValueAsString(principal)) log.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } } }


このファクトリが、 Accountsのルックアップを無効にした理由です。ルックアップ プロセスで Bean を作成できるようにする代わりに、アグリゲーター インスタンスを自分で作成します。 Produces アノテーションを使用すると、Bean を作成できます。カスタム アノテーション AccountsProduct を使用して、この Bean の使用をより具体的にします。最後に、 ApplicationScoped使用して、そのスコープをApplicationスコープとして定義します。つまり、アカウント アグリゲーション Bean は、アプリケーション全体でシングルトン オブジェクトとして動作します。「 createResponse 」は、JSON 応答を作成するための汎用メソッドです。ここで必要なのAccountsResource 、2 つの「リソース」です。これは基本的に Spring の「 Controllers 」と同じです。名前は異なりますが、使用方法はまったく同じです。AccountsResource クラスを見てみましょう。

 @Path("accounts") @RequestScoped @Produces(MediaType.APPLICATION_JSON) open class AccountResource { @Inject @AccountsProduct open var accounts: Accounts? = null @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null @Inject @Claim("access") open var access: JsonString? = null @Claim("iat") @Inject open var iat: JsonNumber? = null @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null @POST @RolesAllowed("admin", "client", "credit") @Throws(JsonProcessingException::class) open fun createAccount(): Response = createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) @POST @RolesAllowed("admin", "user") @Path("user") @Throws(JsonProcessingException::class) open fun createUser(): Response { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) } @GET @RolesAllowed("admin", "client") @Throws(JsonProcessingException::class) open fun getAccount(): Response? { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() ) } @PUT @RolesAllowed("admin", "client") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody): Response? { val userAccount = requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0) requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount return createResponse(currentAccount) } @GET @Path("all") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getAll(): Response? { val allAccounts = ArrayList( requireNotNull(accounts).accountMap .values ) logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(allAccounts) .build() } @GET @Path("summary") @Throws(JsonProcessingException::class) open fun getSummary(): Response? { val totalCredit = requireNotNull(accounts).accountMap .values .map(Account::currentValue) .stream() .reduce { result, u -> result.add(u) } .orElse(BigDecimal.ZERO) val jsonObject = Json.createObjectBuilder() .add("totalCurrent", totalCredit) .add("client", "Mother Nature Dream Team") .build() logger.info("Summary") logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } @GET @RolesAllowed("admin", "client") @Path("jwt") open fun getJWT(): Response? { val jsonObject = Json.createObjectBuilder() .add("jwt", requireNotNull(jsonWebToken).rawToken) .add("userId", requireNotNull(userId).doubleValue()) .add("access", requireNotNull(access).string) .add("iat", requireNotNull(iat).doubleValue()) .build() return Response.ok(jsonObject) .build() } @Throws(JsonProcessingException::class) private fun createResponse(currentAccount: Account): Response = AccountsFactory.createResponse( currentAccount, requireNotNull(name), requireNotNull(accounts), logger, objectMapper, principal, jsonWebToken ) companion object { val objectMapper: ObjectMapper = ObjectMapper() val logger: Logger = LoggerFactory.getLogger(AccountResource::class.java) } }

このクラスをもう少し詳しく見てみましょう。Path アノテーションは、 Pathからこのリソースに到達する方法を定義します。ルートとして「/」を使用していることに注意してください。この場合、「accounts」がこのリソースのルート アクセス ポイントです。すべてのリソース (この場合は 2 つだけ) は、スコープ RequestResource で実行されています。アノテーション Produces により、タイプに関係なくすべてのリクエストに対するすべての応答が JSON 形式のメッセージ形式になることが決定されます。 aggregator挿入するには、Inject アノテーションとAccountsProductアノテーションの組み合わせを使用します。

 @Inject @AccountsProduct open var accounts: Accounts? = null


これは、ファクトリーで定義したものと一致します。さらに、セキュリティの重要な要素であるprincipaljsonWebTokenも挿入しています。

 @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null


JsonWebTokenPrincipal両方とも同じであり、ログでそれを確認できます。リソースでは、特定のトークンを使用してリクエストからクレームを常に挿入できます。

 @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null


これは、 InjectアノテーションとClaimアノテーションの組み合わせで実現されます。Claim アノテーションの下に配置される名前は、 Claimクレームを注入するかを定義します。パラメータを定義する型には注意する必要があります。この例では、 JsonStringJsonNumber型のみが必要です。まず、アカウントとユーザーを作成する方法を見てみましょう。

 @POST @RolesAllowed("admin", "client", "credit") @Throws(JsonProcessingException::class) open fun createAccount(): Response = createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) @POST @RolesAllowed("admin", "user") @Path("user") @Throws(JsonProcessingException::class) open fun createUser(): Response { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) }

アカウントとユーザーの作成


ここでの目的は、メソッドを分離して、異なる権限を付与できるようにすることです。この例では、どちらもアカウントを作成するだけですが、ユーザー ロールを持つユーザーのみが createUser メソッドを使用できることに注意してください。同様に、クライアントとクレジット ロールを持つユーザーのみが createAccount メソッドにアクセスできます。次に、このリソースの PUT リクエスト メソッドを詳しく見てみましょう。

 @PUT @RolesAllowed("admin", "client") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody): Response? { val userAccount = requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0) requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount return createResponse(currentAccount) }

現金化


PUTアノテーションは、このメソッドがPUTタイプのリクエストでのみアクセス可能であることを示していることがわかります。次に、Path アノテーションは、このメソッドへのパスが値であることを Jetty に伝えます。これはPathParamとも呼ばれます。最後に、このメソッドを admin または client のロールを持つユーザーのみが使用できるように定義できます。入力値は、PathParam を使用して Long 値変数に渡されます。ロールを定義しない場合は、適切なトークンを持つすべてのユーザーがこれらのメソッドにアクセスできます。CreditResource CreditResource同様に実装されます。

 @Path("credit") @RequestScoped @Produces(MediaType.APPLICATION_JSON) open class CreditResource { @Inject @AccountsProduct open var accounts: Accounts? = null @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null @Inject @Claim("access") open var access: JsonString? = null @Inject @Claim("iat") open var iat: JsonNumber? = null @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null @GET @RolesAllowed("admin", "credit") @Throws(JsonProcessingException::class) open fun getAccount(): Response = requireNotNull(accounts).let { accounts -> createResponse( accounts.accountMap[requireNotNull(name).string] ?: return Response.serverError().build() ) } @PUT @RolesAllowed("admin", "credit") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody) = requireNotNull(accounts).let { accounts -> requireNotNull(name).let { name -> accounts.accountMap[name.string] = (accounts.accountMap[name.string] ?: return Response.serverError() .build()).addCreditValue(transactionBody.saldo?: 0L) createResponse( (accounts.accountMap[name.string] ?: return Response.serverError() .build()).addCreditValue(transactionBody.saldo?: 0L) ) } } @GET @Path("all") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getAll(): Response? { val allAccounts = ArrayList( requireNotNull(accounts).accountMap .values ) logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(allAccounts) .build() } @GET @Path("summary") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getSummary(): Response? { val totalCredit = requireNotNull(accounts).accountMap .values .map(Account::creditValue) .stream() .reduce { total, v -> total.add(v) } .orElse(BigDecimal.ZERO) val jsonObject = Json.createObjectBuilder() .add("totalCredit", totalCredit) .add("client", "Mother Nature Dream Team") .build() logger.info("Summary") logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } @GET @RolesAllowed("admin", "client") @Path("jwt") open fun getJWT(): Response? { val jsonObject = Json.createObjectBuilder() .add("jwt", requireNotNull(jsonWebToken).rawToken) .add("userId", requireNotNull(userId).doubleValue()) .add("access", requireNotNull(access).string) .add("iat", requireNotNull(iat).doubleValue()) .build() return Response.ok(jsonObject) .build() } @Throws(JsonProcessingException::class) private fun createResponse(currentAccount: Account): Response { return AccountsFactory.createResponse( currentAccount, requireNotNull(name), requireNotNull(accounts), logger, objectMapper, principal, jsonWebToken ) } companion object { val objectMapper: ObjectMapper = ObjectMapper() val logger: Logger = LoggerFactory.getLogger(CreditResource::class.java) } }

唯一の違いは、 adminclientロールを使用する代わりに、 admincreditロールを使用することです。また、このresourceではユーザーのアカウントは作成されないことに注意してください。これは、アカウントのresourceを介してのみ可能です。コードの実装方法がわかったので、まずはRESTサービスでどのメソッドを利用できるようにしたかをまとめてみましょう。

8. アプリケーションの使用

使用されているサービスのリストを確認しましょう:


タイプ、URL、ペイロード、結果、許可されるロール
役職、 http://localhost:8080/accounts,n/a,作成済みアカウント、管理者/クライアント/クレジット
役職、
http://localhost:8080/accounts/user,n/a,作成済みユーザー、管理者/ユーザー
得る、
http://localhost:8080/accounts,n/a,一致アカウント、管理者/クライアント
置く、
http://localhost:8080/accounts、{saldo: Long}、現在の残高、管理者/クライアント
得る、
http://localhost:8080/accounts/all,n/a,すべて当座預金、すべて
得る、
http://localhost:8080/accounts/summary,n/a,合計すべてのバランス、すべて
得る、
http://localhost:8080/credit,n/a,マッチングアカウント、管理者/クライアント
置く、
http://localhost:8080/credit、{サルド: Long}、現在のクレジット、管理者/クライアント
得る、
http://localhost:8080/credit/all,n/a,すべてクレジット、すべて
得る、
http://localhost:8080/credit/summary,n/a,合計
クレジット、すべて

9. テスト環境の生成

ルート フォルダーにbashファイルを作成しました。このファイルは「setupCertificates.sh」と呼ばれます。このファイルの内容を見て、何をするのか理解しましょう。

 #!/bin/bash mkdir -p your-finance-files cd your-finance-files || exit openssl genrsa -out baseKey.pem openssl pkcs8 -topk8 -inform PEM -in baseKey.pem -out privateKey.pem -nocrypt openssl rsa -in baseKey.pem -pubout -outform PEM -out publicKey.pem echo -e '\033[1;32mFirst test\033[0m' java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \ -p ../jwt-plain-tokens/jwt-token-admin.json \ -key ../your-finance-files/privateKey.pem >> token.jwt CERT_PUBLIC_KEY=$(cat ../your-finance-files/publicKey.pem) CERT_ISSUER="joaofilipesabinoesperancinha" echo -e "\e[96mGenerated public key: \e[0m $CERT_PUBLIC_KEY" echo -e "\e[96mIssued by: \e[0m $CERT_ISSUER" echo -e "\e[96mYour token is: \e[0m $(cat token.jwt)" cp ../your-financeje-banking/src/main/resources/config-template ../your-financeje-banking/src/main/resources/config_copy.yml CERT_CLEAN0=${CERT_PUBLIC_KEY//"/"/"\/"} CERT_CLEAN1=${CERT_CLEAN0//$'\r\n'/} CERT_CLEAN2=${CERT_CLEAN1//$'\n'/} CERT_CLEAN3=$(echo "$CERT_CLEAN2" | awk '{gsub("-----BEGIN PUBLIC KEY-----",""); print}') CERT_CLEAN4=$(echo "$CERT_CLEAN3" | awk '{gsub("-----END PUBLIC KEY-----",""); print}') CERT_CLEAN=${CERT_CLEAN4//$' '/} echo -e "\e[96mCertificate cleanup: \e[0m ${CERT_CLEAN/$'\n'/}" sed "s/{{ publicKey }}/$CERT_CLEAN/g" ../your-financeje-banking/src/main/resources/config_copy.yml > ../your-financeje-banking/src/main/resources/config_cert.yml sed "s/{{ issuer }}/$CERT_ISSUER/g" ../your-financeje-banking/src/main/resources/config_cert.yml > ../your-financeje-banking/src/main/resources/config.yml rm ../your-financeje-banking/src/main/resources/config_cert.yml rm ../your-financeje-banking/src/main/resources/config_copy.yml echo -e "\e[93mSecurity elements completely generated!\e[0m" echo -e "\e[93mGenerating tokens...\e[0m" TOKEN_FOLDER=jwt-tokens mkdir -p ${TOKEN_FOLDER} # CREATE_ACCOUNT_FILE=createAccount.sh CREATE_USER_FILE=createUser.sh SEND_MONEY_FILE=sendMoney.sh ASK_CREDIT_FILE=askCredit.sh TOKEN_NAME_VALUE=tokenNameValue.csv echo "#!/usr/bin/env bash" > ${CREATE_ACCOUNT_FILE} chmod +x ${CREATE_ACCOUNT_FILE} echo "#!/usr/bin/env bash" > ${CREATE_USER_FILE} chmod +x ${CREATE_USER_FILE} echo "#!/usr/bin/env bash" > ${SEND_MONEY_FILE} chmod +x ${SEND_MONEY_FILE} echo "#!/usr/bin/env bash" > ${ASK_CREDIT_FILE} chmod +x ${ASK_CREDIT_FILE} for item in ../jwt-plain-tokens/jwt-token*.json; do if [[ -f "$item" ]]; then filename=${item##*/} per_token=${filename/jwt-token-/} token_name=${per_token/.json/} cp "${item}" jwt-token.json java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \ -p jwt-token.json \ -key ../your-finance-files/privateKey.pem > token.jwt cp token.jwt ${TOKEN_FOLDER}/token-"${token_name}".jwt token=$(cat token.jwt) echo "# Create account: ""${token_name}" >> ${CREATE_ACCOUNT_FILE} echo "echo -e \"\e[93mCreating account \e[96m${token_name}\e[0m\"" >> ${CREATE_ACCOUNT_FILE} echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X POST >> ${CREATE_ACCOUNT_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_ACCOUNT_FILE} echo "# Create user: ""${token_name}" >> ${CREATE_USER_FILE} echo "echo -e \"\e[93mCreating user \e[96m${token_name}\e[0m\"" >> ${CREATE_USER_FILE} echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts/user -X POST >> ${CREATE_USER_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_USER_FILE} echo "# Send money to: "${token_name} >> ${SEND_MONEY_FILE} echo "echo -e \"\e[93mSending money to \e[96m${token_name}\e[0m\"" >> ${SEND_MONEY_FILE} echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'" >> ${SEND_MONEY_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${SEND_MONEY_FILE} echo "# Asking money credit to: "${token_name} >> ${ASK_CREDIT_FILE} echo "echo -e \"\e[93mAsking credit from \e[96m${token_name}\e[0m\"" >> ${ASK_CREDIT_FILE} echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/credit -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'">> ${ASK_CREDIT_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${ASK_CREDIT_FILE} echo "${token_name},${token}" >> ${TOKEN_NAME_VALUE} fi done

環境生成

ファイルの説明に従って、その内容を確認してください。これは、その内容を正確に理解するために重要です。まず、 PEM形式で秘密鍵と公開鍵を作成します。次に、実行可能な「your-finance-jwt-generator.jar」で秘密鍵を使用します。これは、トークンをすばやく作成できる実行可能な jar です。発行者は後で変更できません。最後に、トークンを作成します。このトークンの読み方は後で説明します。このトークンには、3 つの追加ヘッダー クレームが含まれています。これらは、「kid」、「typ」、および「alg」です。次の形式に従います。

 { "kid": "jwt.key", "typ": "JWT", "alg": "RS256" }

JWTのヘッダー

これらの主張をもう少し詳しく見てみましょう。

  • 「kid」 — ヒントクレームとして機能します。これは、使用しているアルゴリズムの種類を示します。
  • 「typ」 — IANAメディア タイプを宣言するために使用されます。JWT (JSON Web トークン)、 JWE (JSON Web 暗号JWT )、 JWA (JSON Web アルゴリズム) の 3 つのオプションがあります。これらのタイプは、この実験には関係ありません。トークンが十分に暗号化されておらず、復号化が非常に簡単であることがわかります。また、トークンを復号化することはできますが、他のアクションを実行するために簡単に改ざんすることはできないこともわかります。
  • 「alg」 — 使用する署名の種類を定義する方法です。署名は、元のトークンが変更されておらず、信頼されていることを保証する暗号化操作と考えることができます。この場合、SHA-256 を使用した RSA 署名とも呼ばれる RS256 を使用しています。

公開キーを使用すると、最終的にテンプレートを変更するために使用できます。新しい config.yml ファイルは次のようになります。

 kumuluzee: name: your-financeje-banking version: 1.0.0 jwt-auth: public-key: FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKEN issuer: joaofilipesabinoesperancinha healthy: true

config.yml


2 番目のステップは、4 つのファイルを作成することです。ディレクトリ「 jwt-plain-tokens 」内の各プレーン トークンに対して、4 つのコマンドを作成します。最初のコマンドは、アカウントで効果的に操作できるユーザーを作成することです。これらは、プロファイル「 admin 」、「 client 」、「 credit 」を持つユーザーです。これらを作成するには、「 createAccount.sh 」ファイルを実行します。2 番目のコマンドは、まだ権限を持っていない残りのユーザーを作成します。これは「createUser.sh」ファイルです。これを実行してみましょう。これで、すべてのユーザーが最終的に作成されたことがわかります。次に、トランザクションの詳細を確認し、残りの 2 つのコマンドを見てみましょう。1 つは「cashin」コマンドで、もう 1 つはクレジットを増やすよう要求するコマンドです。最初に生成されるファイルは、「sendMoney.sh」bash スクリプトです。ここに、「 cashin 」へのすべてのリクエストがあります。このファイルには、ユーザーごとにランダムな金額をユーザーに送信するための curl リクエストがあります。管理者の場合を見てみましょう。

 #!/usr/bin/env bash # Send money to: admin echo -e "\e[93mSending money to \e[96madmin\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer= FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 125}' echo -e "\e[93m\n---\e[0m" # Send money to: cindy echo -e "\e[93mSending money to \e[96mcindy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 360}' echo -e "\e[93m\n---\e[0m" # Send money to: faustina echo -e "\e[93mSending money to \e[96mfaustina\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 50}' echo -e "\e[93m\n---\e[0m" # Send money to: jack echo -e "\e[93mSending money to \e[96mjack\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 205}' echo -e "\e[93m\n---\e[0m" # Send money to: jitska echo -e "\e[93mSending money to \e[96mjitska\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 332}' echo -e "\e[93m\n---\e[0m" # Send money to: judy echo -e "\e[93mSending money to \e[96mjudy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 295}' echo -e "\e[93m\n---\e[0m" # Send money to: lucy echo -e "\e[93mSending money to \e[96mlucy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 160}' echo -e "\e[93m\n---\e[0m" # Send money to: malory echo -e "\e[93mSending money to \e[96mmalory\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 413}' echo -e "\e[93m\n---\e[0m" # Send money to: mara echo -e "\e[93mSending money to \e[96mmara\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 464}' echo -e "\e[93m\n---\e[0m" # Send money to: namita echo -e "\e[93mSending money to \e[96mnamita\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 51}' echo -e "\e[93m\n---\e[0m" # Send money to: pietro echo -e "\e[93mSending money to \e[96mpietro\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 491}' echo -e "\e[93m\n---\e[0m" # Send money to: rachelle echo -e "\e[93mSending money to \e[96mrachelle\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 474}' echo -e "\e[93m\n---\e[0m" # Send money to: sandra echo -e "\e[93mSending money to \e[96msandra\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 417}' echo -e "\e[93m\n---\e[0m" # Send money to: shikka echo -e "\e[93mSending money to \e[96mshikka\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 64}' echo -e "\e[93m\n---\e[0m"

sendMoney.sh 抽出

同じユーザーにはクレジットリクエストも割り当てられます。

 #!/usr/bin/env bash # Asking money credit to: admin echo -e "\e[93mAsking credit from \e[96madmin\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 137}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: cindy echo -e "\e[93mAsking credit from \e[96mcindy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 117}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: faustina echo -e "\e[93mAsking credit from \e[96mfaustina\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 217}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: jack echo -e "\e[93mAsking credit from \e[96mjack\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 291}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: jitska echo -e "\e[93mAsking credit from \e[96mjitska\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 184}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: judy echo -e "\e[93mAsking credit from \e[96mjudy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 388}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: lucy echo -e "\e[93mAsking credit from \e[96mlucy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 219}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: malory echo -e "\e[93mAsking credit from \e[96mmalory\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 66}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: mara echo -e "\e[93mAsking credit from \e[96mmara\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 441}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: namita echo -e "\e[93mAsking credit from \e[96mnamita\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 358}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: pietro echo -e "\e[93mAsking credit from \e[96mpietro\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 432}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: rachelle echo -e "\e[93mAsking credit from \e[96mrachelle\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 485}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: sandra echo -e "\e[93mAsking credit from \e[96msandra\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 500}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: shikka echo -e "\e[93mAsking credit from \e[96mshikka\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 89}' echo -e "\e[93m\n---\e[0m"

askCredit.sh の抜粋



私たちのcharactersすべてNature連盟の一部です。基本的には、この銀行システムの一部である人々のグループです。この文脈では、彼らは環境を守っています。このグループの人々が何をしているのか、ストーリーのどこに当てはまるのかは記事にはあまり関係ありませんが、文脈上、彼らは環境を守り、気候変動の影響を遅らせる活動に参加しています。私たちのcharactersの中には何でもできる人もいれば、何もできない人もいれば、「現金化」または「クレジットの要求」しかできない人もいます。また、機密情報を難読化していることにも注意してください。これらのトークンは通常、共有したり、特定の URL で表示したりしないでください。ブラウザの開発者コンソールからは常に利用できますが、いずれにしても、行われているリクエストprotectためです。これは「security-per-obscurity」と呼ばれる概念でありand技術的にはユーザーが使用されているトークンに気付くのを防ぐことはできませんが、抑止力として機能します。どちらの方法でも、入金を行うときやクレジットを要求するときは、リクエストごとに 1 ~ 500 のランダムな数字が送信されることに注意してください。これでアプリケーションを開始する準備がほぼ整いましたが、まずは理論をもう少し詳しく見てみましょう。

10. JWTトークンはどのように作られるか




トークンを生成したので、その 1 つを見てみましょう。難読化されたトークンを示し、これFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO使用してこれを理解します。これが私たちのトークンです: FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE ... FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENここで注目すべき重要な点は、トークンが 3 つの部分に分割されていることです。

  • ヘッダー— これは、上で説明したように、Base64 でエンコードされた JSON 構成ヘッダーです。
  • ペイロード— これは、Base64 でエンコードされた JSON ペイロードです。ここで、予約済みクレームとカスタム クレームを定義しました。また、ここでプライベート クレームとパブリック クレームを定義することもできます。これらは両方ともカスタム クレームに分類されます。簡単に言うと、これらのクレームは両方とも自由に使用できます。ただし、パブリック クレームは、IANA JSON Web トークン レジストリで定義されているクレームを指します。レジストリとの衝突を避けるために、トークンに特定の方法で名前を付けることが重要です。パブリック クレームは、標準オプションとして定義することもできます。プライベート クレームは標準に従わず、定義するのは私たち次第です。
  • 署名— ここで少し創造的になることができます。署名は、 HeaderPayloadの暗号化された組み合わせです。使用するアルゴリズムを決定し、トークンのこの部分によって、送信するメッセージが信頼できるかどうかが基本的に決定されます。これはその組み合わせに固有のものであり、サーバーは作成した「公開キー」を使用して、一致するかどうかを判断します。上記の例でRS256使用していることを覚えていると思います。


先に進む前に、この例ではHeaderPayload両方がdecyphered可能であることに注意してください。ペイロードまたはヘッダーを改ざんしても、信頼できるものにすることはできません。悪意のあるトークンの潜在的な影響に対する保護は、選択したアルゴリズムによってのみ保護できます。したがって、慎重に選択してください。銀行など、極秘情報が懸念される組織で働いている場合は、これから行うことは絶対にしないでください。これは、ローカルで生成したトークンの内容をオンラインで確認するための手段にすぎません。まず、 https://jwt.io/にアクセスして、 JWTトークンを入力します。生成したトークンを使用します。


https://jwt.io/を使用してトークンの内容を確認します。ここで何があるか確認してみましょう。これは管理者トークンです。この例では、その人物は「Admin」です。パラメータがすべて使用可能であることがわかります。リストには、「sub」、「aud」、「upn」、「access」、「user_id」、「iss」、「name」、「groups」、最後に「jti」があります。また、追加のクレームもいくつかあります。これらを見てみましょう。



auth_time 」 — これは認証が行われた時刻です。トークンは、2022 年 7 月 17 日日曜日 16:15:47 GMT+02:00 DST に認証されました。「 iat 」 — これはトークンが作成された時刻です。この場合、これは auth_time と同時に発生します。「 exp 」 — これはトークンの有効期限です。有効期限は 2022 年 7 月 17 日日曜日 16:32:27 GMT+02:00 DST です。トークンに有効期限を指定しませんでした。つまり、 JWTデフォルト値の約 15 分を使用します。

それでは、いくつかテストを実行してみましょう。

11. アプリケーションの実行

コードはGitHubで使用できる状態になっています。コードをチェックアウトして Intellij で開く場合、このアプリケーションを Spring Boot アプリケーションのように実行できないことに注意する必要があります。実行するための「psvm」はありません。代わりに、生成された jar を直接実行し、その直前に「mvn build」を行うようにしてください。現時点では、次のように使用しています。

[ ] https://github.com/jesperancinha/your-finance-je 「アプリケーションを実行するための環境設定」)



ここで、「 setupCertificates.sh 」スクリプトをもう一度実行してみましょう。ここまでにどのくらいの時間がかかったかはわかりませんが、この時点ですでに 15 分が経過している可能性が高いです。念のため、もう一度実行してください。アプリを起動してみましょう。次のように起動できます。

 mvn clean install java -jar your-financeje-banking/target/your-financeje-banking.jar

または、すぐに実行できる構成で実行することもできます。すべての動作を理解したい場合は、事前にリポジトリと Makefile を確認してください。

 make dcup-full-action

このスクリプトは 2 つのサービスを実行します。1 つはポート8080で、もう 1 つはポート8081です。ポート8080では、 JWTトークンを生成するために独自のコードを実行するこのソフトウェアのバージョンを実行します。ポート 8081 では、 Adam Bienが作成したjwtknizrジェネレーターを使用するバージョンを実行します。ただし、この記事ではポート8080で実行されているサービスに焦点を当てます。必要に応じて、次のコマンドでcypress実行することもできます。

 make cypress-open

これにより、 cypressコンソールがopen 、選択したブラウザでテストを実行できるようになります。ただし、この段階ではブラウザのオプションはまだ限られています。リクエストのほとんどは、実際にはcypressによって提供されるコマンド ライン リクエストになります。今のところ、「 cypress 」には立ち入りません。ブラウザで次の場所に移動してください。

http://localhost:8080/アカウント/すべて

次のような結果が得られるはずです:


ご覧のとおり、「 Malory 」、「 Jack Fallout 」、および「 Jitska 」にはクレジットもお金もありません。これは、これらのクライアントにユーザー グループのみが与えられているためです。また、 Shikkaはクレジットが与えられていないことにも注意してください。「 Shikka 」は、グループ クレジットを持たない唯一のクライアントです。ログを見ると、成功した操作は次の形式になっていることがわかります。

 Sending money to admin HTTP/1.1 200 OK Date: Sun, 17 Jul 2022 15:01:13 GMT X-Powered-By: KumuluzEE/4.1.0 Content-Type: application/json Content-Length: 32 Server: Jetty(10.0.9) {"balance":212,"client":"Admin"}


200 は、操作が正常に完了したことを示します。「Malory」、「Jack Fallout」、および「Jitska」の場合、両方の操作が失敗し、次のようなメッセージが表示されます。

 Sending money to jitska HTTP/1.1 403 Forbidden X-Powered-By: KumuluzEE/4.1.0 Content-Length: 0 Server: Jetty(10.0.9)

403 は、 JWTトークンが検証され、信頼されていることを知らせます。ただし、ユーザーはその操作を実行することが禁止されています。つまり、指定されたメソッドにアクセスできないということです。

トークンを少し変更してみましょう。sendMoney.sh ファイルのトークンの一部を変更すると、次のようになります。

 Sending money to admin HTTP/1.1 401 Unauthorized X-Powered-By: KumuluzEE/4.1.0 WWW-Authenticate: Bearer realm="MP-JWT" Content-Length: 0 Server: Jetty(10.0.9)

フルスクリーンモードに入る フルスクリーンモードを終了する

この401トークンが検証されなかったことを意味します。つまり、サーバーがトークンが信頼できるかどうかを確認するために使用する公開キーが一致しなかったことを意味します。公開キーが JWT トークンの署名を評価および検証できない場合、トークンは拒否されます。

要約すると、ヘッダーと「ペイロード」は暗号化されていません。これらは単に Base 64 で「エンコード」されています。つまり、「デコード」により、ペイロードの実際の内容をいつでも確認することができます。ペイロードを盗聴から保護したい場合は、トークンの「ペイロード」を識別パラメータの選択以外に使用しないでください。実際に問題となるのは、誰かがJWTトークンを入手した場合です。たとえば、TLS トンネルが侵害され、交換されたメッセージの内容を誰かが読み取ることができる場合などです。その場合、別の保護手段があります。それが署名です。受信メッセージを検証できるのは、公開鍵を含むサーバーだけです。この公開鍵は公開されていますが、署名と「ヘッダー + ペイロード」に対して実行することによってのみ、受信メッセージを検証できます。

12. 結論

セッションはこれで終了です。お付き合いありがとうございました。JWT トークンはコンパクトで、XML 版のJWTトークンに比べてはるかに簡潔であることがおわかりいただけたと思います。トークンを作成して使用して特定SAMLメソッドに必要な特定の承認を取得するのがいかに簡単か、また署名付きトークンを使用してそこに到達する方法も確認しました。ただし、 JWTどのように機能するかを理解することは非常に重要だと思います。これで、 JWTトークンがどのように機能するかについて、良い紹介ができたと思います。これらすべてがどのように機能するかをよりよく理解するには、実装されたcypressテストを試してみることをお勧めします。これは、リクエストがどのように行われているか、何をテストしているのか、何が期待されているのかを確認するのに最適な方法です。また、一部のユーザーが特定の操作を実行できるのに、他のユーザーが実行できない理由もよりよく理解できます。このアプリケーションのソース コードはすべてGitHubに置きました。この記事を執筆するのを楽しんだのと同じくらい、皆さんもこの記事を楽しんでいただければ幸いです。お読みいただきありがとうございました。

13. 参考文献