CLOVER🍀

That was when it all began.

OKD/Minishiftで、Across Environments Image Promotion

これは、なにをしたくて書いたもの?

  • OpenShift(OKD)上で、Project(Namespace)を跨いでアプリケーションを使いたい
  • イメージ的には、開発用Projectで作ったアプリケーションを、本番用Projectに展開するといった感じ

このような話を、Promotionと呼ぶようです。

今回は、こちらを試してみます。

お題

上述のそのままで、以下のことをやります。

  • 開発用Projectと、そのProjectに所属するユーザーを作成する
  • 本番用Projectと、そのProjectに所属するユーザーを作成する
  • 開発用Projectに、単純なアプリケーションをGitリポジトリを指定してデプロイする
  • 本番用Projectからは、開発用Projectで作成したアプリケーションのイメージをデプロイする
  • 開発用Projectでアプリケーションをアップデートし、本番用Projectに反映する

Promoting Applications Across Environments

参考にしたのは、こちらのドキュメントとブログエントリ。

Promoting Applications Across Environments - Application Life Cycle Management | Developer Guide | OKD 3.11

Promoting Applications Across Environments – Red Hat OpenShift Blog

開発用Projectでビルドとデプロイを行い、本番用ProjectにPromotionする流れが書いてあります。

もちろん、アプリケーションのイメージだけあってもダメなので、RouteやConfigMapとかSecretとか、いろいろ個別に
考慮するものはあるようなのですが、今回はシンプルにアプリケーションにフォーカスします。
※Promoting Applications Across Environments - Application Life Cycle Management | Developer Guide | OKD 3.11参照

環境

今回の環境は、こちら。

$ minishift version
minishift v1.27.0+707887e


$ oc version
oc v3.11.0+0cbc58b
kubernetes v1.11.0+d4cacc0
features: Basic-Auth GSSAPI Kerberos SPNEGO

Server https://192.168.42.132:8443
kubernetes v1.11.0+d4cacc0

サンプルアプリケーション

今回は、Node.jsで簡単なサンプルアプリケーションを作成します。

Expressを使った、サーバーにしましょう。

$ npm i express

インストールされたExpressのバージョンは、こちら。

  "dependencies": {
    "express": "^4.16.4"
  }

アプリケーションは、単にバージョン付きのメッセージを返すだけにします。 server.js

const express = require('express');
const app = express();

const version = '1.0';

app.get('/', (req, res) => res.send(`this app version = ${version}`));

console.log(`[${new Date()}] server startup.`);

app.listen(8080);

package.json上のscripts指定は、こんな感じで。

  "scripts": {
    "start": "node server.js"
  },

このソースコードを、Gitリポジトリに登録しておきます。

開発用Projectでのビルド・デプロイと、本番用ProjectへのPromotion

では、作成したアプリケーションをデプロイしたりしていきましょう。

まず、OKD上に開発用ProjectのユーザーとProjectそのものを作成します。

## 開発用のユーザー&Project
$ oc login https://$(minishift ip):8443 -u dev-user1
$ oc new-project dev


## 本番用のユーザー&Project
$ oc login https://$(minishift ip):8443 -u prod-user1
$ oc new-project prod

開発用のユーザーでログインし、開発用Projectに切り替えます。

$ oc login https://$(minishift ip):8443 -u dev-user1
$ oc project dev

アプリケーションのデプロイと、Routeの作成。アプリケーション名は「simple-app」としています。

$ oc new-app [GitリポジトリのURL]
$ oc expose svc/simple-app

動作確認。OKですね。

$ curl simple-app-dev.192.168.42.132.nip.io
this app version = 1.0

で、今回作成したイメージに、タグを作ります。今の「latest」を「promote」というタグで指定しました。

$ oc tag dev/simple-app:latest dev/simple-app:promote

「oc describe」すると、こんな感じ。今は「latest」と「promote」で、ImageStreamが指しているハッシュの値が同じですね。

$ oc describe is
Name:           simple-app
Namespace:      dev
Created:        About a minute ago
Labels:         app=simple-app
Annotations:        openshift.io/generated-by=OpenShiftNewApp
Docker Pull Spec:   172.30.1.1:5000/dev/simple-app
Image Lookup:       local=false
Unique Images:      1
Tags:           2

latest
  no spec tag

  * 172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      About a minute ago

promote
  tagged from simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67

  * 172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      8 seconds ago

では、本番用ユーザーでログインして、Projectも切り替えて

$ oc login https://$(minishift ip):8443 -u prod-user1
$ oc project prod

このアプリケーションをデプロイしようとすると…失敗します。

$ oc new-app dev/simple-app:promote
error:  local file access failed with: stat dev/simple-app:promote: no such file or directory
error: unable to locate any images in image streams, templates loaded in accessible projects, template files, local docker images with name "dev/simple-app:promote"

本番用のユーザーやプロジェクトから、開発用のProjectの情報が見えていません、と。

ここで、1度開発用のユーザーに切り替え、

$ oc login https://$(minishift ip):8443 -u dev-user1

本番用のユーザーに参照権限の付与と

$ oc policy add-role-to-user view prod-user1
role "view" added: "prod-user1"

本番用のデフォルトのServiceAccountである「prod」に「system:image-puller」ロールを与えます。

$ oc policy add-role-to-group system:image-puller system:serviceaccounts:prod -n dev
role "system:image-puller" added: "system:serviceaccounts:prod"

Service Accounts | Developer Guide | OKD 3.11

これで、本番用のユーザーから開発用Projectの内容が参照でき、またイメージのpullができるようになります。

本番用ユーザーとプロジェクトに切り替え。

$ oc login https://$(minishift ip):8443 -u prod-user1
$ oc project prod

なお、この時本番用のユーザーからは、開発用ProjectがProjectの一覧に表示されるようになっています。

$ oc projects
You have access to the following projects and can switch between them with 'oc project <projectname>':

    dev
  * prod

Using project "prod" on server "https://192.168.42.132:8443".

タグで指定したイメージを使って、再度「oc new-app」。今度は成功します。

$ oc new-app dev/simple-app:promote

ただ、この状態だとRouteがないので、exposeしておきます。

$ oc expose svc/simple-app

「oc get all」で確認。デプロイできているようです。

$ oc get all
NAME                     READY     STATUS    RESTARTS   AGE
pod/simple-app-1-lr8w6   1/1       Running   0          21s

NAME                                 DESIRED   CURRENT   READY     AGE
replicationcontroller/simple-app-1   1         1         1         22s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/simple-app   ClusterIP   172.30.188.192   <none>        8080/TCP   24s

NAME                                            REVISION   DESIRED   CURRENT   TRIGGERED BY
deploymentconfig.apps.openshift.io/simple-app   1          1         1         config,image(simple-app:promote)

NAME                                        DOCKER REPO                       TAGS      UPDATED
imagestream.image.openshift.io/simple-app   172.30.1.1:5000/prod/simple-app   promote   

NAME                                  HOST/PORT                               PATH      SERVICES     PORT       TERMINATION   WILDCARD
route.route.openshift.io/simple-app   simple-app-prod.192.168.42.132.nip.io             simple-app   8080-tcp                 None

ちなみに、ビルドはしていないので、BuildConfigはありません。

$ oc get bc
No resources found.

アプリケーションの動作確認。

$ curl simple-app-prod.192.168.42.132.nip.io
this app version = 1.0

OKですね。これで、開発用Projectから本番用Projectへ、アプリケーションのPromotionができました。

アプリケーションを修正してみる

続いて、アプリケーションを修正して、再度ビルド&デプロイしてみましょう。

開発用ユーザー&Projectに切り替え。

$ oc login https://$(minishift ip):8443 -u dev-user1
$ oc project dev

アプリケーションのバージョン表記を「2.0」にして、Gitリポジトリに反映します。

const version = '2.0';

ビルド&デプロイ。

$ oc start-build bc/simple-app

しばらく待っていると、デプロイが完了しアプリケーションの更新が反映されます。

$ curl simple-app-dev.192.168.42.132.nip.io
this app version = 2.0

本番用Projectの方は、変わっていません。

$ curl simple-app-prod.192.168.42.132.nip.io
this app version = 1.0

これは、先ほど作成したイメージの「promote」タグが指しているイメージが変わっていないからです。

$ oc describe is/simple-app
Name:           simple-app
Namespace:      dev
Created:        26 minutes ago
Labels:         app=simple-app
Annotations:        openshift.io/generated-by=OpenShiftNewApp
Docker Pull Spec:   172.30.1.1:5000/dev/simple-app
Image Lookup:       local=false
Unique Images:      2
Tags:           2

latest
  no spec tag

  * 172.30.1.1:5000/dev/simple-app@sha256:10ed61895c1866e39394b90a3c89e355349ed63b1addcb6b9febfb89035561f4
      30 seconds ago
    172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      26 minutes ago

promote
  tagged from simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67

  * 172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      25 minutes ago

再度、タグを振り直します。

$ oc tag dev/simple-app:latest dev/simple-app:promote

「promote」タグが最新化されました。

$ oc describe is/simple-app
Name:           simple-app
Namespace:      dev
Created:        27 minutes ago
Labels:         app=simple-app
Annotations:        openshift.io/generated-by=OpenShiftNewApp
Docker Pull Spec:   172.30.1.1:5000/dev/simple-app
Image Lookup:       local=false
Unique Images:      2
Tags:           2

latest
  no spec tag

  * 172.30.1.1:5000/dev/simple-app@sha256:10ed61895c1866e39394b90a3c89e355349ed63b1addcb6b9febfb89035561f4
      50 seconds ago
    172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      26 minutes ago

promote
  tagged from simple-app@sha256:10ed61895c1866e39394b90a3c89e355349ed63b1addcb6b9febfb89035561f4

  * 172.30.1.1:5000/dev/simple-app@sha256:10ed61895c1866e39394b90a3c89e355349ed63b1addcb6b9febfb89035561f4
      3 seconds ago
    172.30.1.1:5000/dev/simple-app@sha256:3271132ce05787a6f24919eb4501979d87068e1b9fb56d6b13152db261eb0c67
      25 minutes ago

すると、本番用Projectの方にも自動でデプロイされています。

$ curl simple-app-prod.192.168.42.132.nip.io
this app version = 2.0

これ、どうなっているんでしょう?

本番用ユーザー&Projectに切り替えて、確認してみます。

$ oc login https://$(minishift ip):8443 -u prod-user1
$ oc project prod

DeploymentConfigの内容を見てみます。「spec.triggers.imageChangeParams.automatic」の部分が関係ありそうですね。

$ oc get dc/simple-app -o yaml
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:

...

spec:

...

  template:

...

  triggers:
  - type: ConfigChange
  - imageChangeParams:
      automatic: true
      containerNames:
      - simple-app
      from:
        kind: ImageStreamTag
        name: simple-app:promote
        namespace: dev
      lastTriggeredImage: 172.30.1.1:5000/dev/simple-app@sha256:10ed61895c1866e39394b90a3c89e355349ed63b1addcb6b9febfb89035561f4
    type: ImageChange

...

ここで、DeploymentConfigを修正してみましょう。

$ oc edit dc/simple-app

「automatic: true」の部分を削除します(「automatic: false」にして保存しても、削除されます)。

  triggers:
  - type: ConfigChange
  - imageChangeParams:
      containerNames:
      - simple-app
      from:
        kind: ImageStreamTag
        name: simple-app:promote
        namespace: dev
      lastTriggeredImage: 172.30.1.1:5000/dev/simple-app@sha256:e9d8d5a09337c25cae7f3aa4b4c05be788e28f180ba15b7c828ccc5d213838df
    type: ImageChange

もう1度、アプリケーションを修正してデプロイしてみましょう。

開発用ユーザー&Projectに切り替え。

$ oc login https://$(minishift ip):8443 -u dev-user1
$ oc project dev

アプリケーションのバージョンを今度は「3.0」にして、Gitリポジトリへ反映。

const version = '3.0';

ビルド&デプロイ。

$ oc start-build bc/simple-app

開発用Projectの方には反映されました。

$  curl simple-app-dev.192.168.42.132.nip.io
this app version = 3.0

では、タグを付けなおしてみます。

$ oc tag dev/simple-app:latest dev/simple-app:promote

ですが、今度は本番用Projectには反映されていません。

$ curl simple-app-prod.192.168.42.132.nip.io
this app version = 2.0

では、この場合どうやって反映するかですが「oc rollout」します。

本番用ユーザー&Projectに切り替え。

$ oc login https://$(minishift ip):8443 -u prod-user1
$ oc project prod

「oc rollout latest」を実行。

$ oc rollout latest dc/simple-app

今度は、反映されました。

$ curl simple-app-prod.192.168.42.132.nip.io
this app version = 3.0

こんなところで確認はOKでしょうか。

まとめ

OKD(Minishift)上で、Projectを跨いだアプリケーションのPromotionを試してみました。

最初、Projectを別にしても、ユーザーをわけなかったら、ロールを付与しなくてもあっさりとイメージのPromotionに成功して
「あれ?ブログに書かれている設定は…?」とか思ったりしたのですが、権限まわりをちゃんと把握しないとダメだなぁと
思いましたねぇ。

とりあえず、このネタはけっこう気になっていたことなので、自分で実践してみて少しは雰囲気がわかったと思います。

参考情報

Promoting Applications Across Environments - Application Life Cycle Management | Developer Guide | OKD 3.11

Promoting Applications Across Environments – Red Hat OpenShift Blog

Cross-Cluster Image Promotion Techniques – Red Hat OpenShift Blog

Building Declarative Pipelines with OpenShift DSL Plugin – Red Hat OpenShift Blog

Multiple Deployment Methods for OpenShift – Red Hat OpenShift Blog

OpenShift with Jenkins for dev/prod parity – /techblog

Java JWTでJWT

これは、なにをしたくて書いたもの?

  • 最近、ちょっとJWTについて知らないといけないなぁと思うできごとがありまして
  • 裏の仕組みとしてJWTを使っているのもいいのですが、もう少しJWT自体に向き合ってみようと
  • なにかしらJWTを扱えるライブラリを使って試しつつ、感覚を掴んでみる

という、とにかくJWTを扱ってみようという話。

Java JWT

JWTを扱うのには、Java JWTというライブラリを選んでみました。

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

jwt.ioにも載っているライブラリで、GitHubのスター数も多めかなと。

JSON Web Tokens - jwt.io

そもそも、JWT自体は?

仕様を読もう。

JSON Web Token (JWT)

JWTによるJSONに対する電子署名と、そのユースケース | DevelopersIO

このあたりを読んでいて、JWT(JSON Web Token)、JWS(JSON Web Signature)、JWE(JSON Web Encryption)の
関係がちょっとわかる感じですねぇ。

JSON Web Token (JWT)

JSON Web Token (JWT)

クレームのセットをJSONオブジェクトとして文字列表現にしてJWSやJWEにエンコードすることで, クレームに対するデジタル署名やMACと暗号化の両方が可能になる.

JSON Web Signature (JWS)

JSON Web Signature (JWS)

デジタル署名もしくはMAC化されたメッセージを表現するデータ構造. JWSは以下の3つの値より構成される: JWS ヘッダ, JWS ペイロード, JWS 署名

仕様書のJWTの例を見ていくと、JSONであるJWTヘッダ、JWTクレーム・セットからJWSが作られていくさまが
わかりますね。

JWTの例

Java JWTの「Signed JWTs」を見ると、その様子がコードで書かれているので、こちらの方が感覚をつかみやすいかも
しれません。

Signed JWTs

JWSを構成する文字列は、Base64で簡単にデコードでき、JWTヘッダ、JWTクレーム・セット、署名となり、内容自体は誰でも読める、
という感じですね(改ざん防止は署名でできる)。

Java JWTを使ってみる

というわけで、ここまで前置きにしてJava JWTでちょっと遊んでみましょう。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-1ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


$ mvn -version
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T03:41:47+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-39-generic", arch: "amd64", family: "unix"
準備

Maven依存関係は、こちら。

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>

jjwt-apiとjjwt-implはとりあえず必要なのと、JSONを扱うためのライブラリが必要です。Java JWTはJacksonとJSON-Javaの
シリアライザー/デシリアライザーを提供しており、このどちらかを使うことになります。

なお、jjwt-api以外は、いずれもスコープはruntimeでOKです。

あとは、テストコードで確認するので、テストライブラリを追加。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.11.1</version>
            <scope>test</scope>
        </dependency>
テストコードの雛形

テストコードの雛形は、こちら。
src/test/java/org/littlewings/jjwt/JjwtTest.java

package org.littlewings.jjwt;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.crypto.spec.SecretKeySpec;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class JjwtTest {

    // ここに、テストを書く!!
}

ここに、テストを足していきたいと思います。

簡単なJava JWTのサンプル

とりあえず、書いてみたのはこんな感じ。

    @Test
    public void gettingStarted() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

秘密鍵はランダムUUIDで適当に生成、JWTクレーム・セットはsubjectのみです。

JWTを作っているのは、この部分です。

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .compact();

アルゴリズムは、鍵の長さに合わせて選択してもらいました。

選択肢は、このバージョンだと「HmacSHA512」、「HmacSHA384」、「HmacSHA256」から選ばれます。

生成されたJWTは、こんな感じになります。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoifQ.wHj5Yg1P9lh4MXQuf5ViXqgHeA9thpZ0vsvfIGAeZ80

パースしてみましょう。

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .parseClaimsJws(jwsString);

JW"S"としてパースします。

System.out.printlnしてみると、こんな感じになっています。

parsed JWT = header={alg=HS256},body={sub=磯野カツオ},signature=wHj5Yg1P9lh4MXQuf5ViXqgHeA9thpZ0vsvfIGAeZ80

中身を確認。

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

今回の秘密鍵では、「HmacSHA256」アルゴリズムが選択されたようです(JWT上は「HS512」)。

Java JWTのサンプルでは、秘密鍵を自動生成するサンプルになっているのですが、こちらはまあいいかなと。
※アルゴリズムは明示してある

Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
アルゴリズムを明示する

先ほどはアルゴリズムをライブラリ側に選んでもらっていましたが、今度は明示してみます。

    @Test
    public void specifiedAlgorithm() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

SecretKeySpecを使って、指定しました。

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

これは、先ほどのライブラリ側で選んでもらっている場合に、内部的に使われている方法と同じなのですがね…。

JWTと、パースした結果はこちらです。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoifQ.zr8C4O7IlDpHrnr-8gcEUtlFTMihFL0GAGppGklGhx4
parsed JWT = header={alg=HS256},body={sub=磯野カツオ},signature=zr8C4O7IlDpHrnr-8gcEUtlFTMihFL0GAGppGklGhx4
JWTに有効期限を設定する

続いて、JWTに有効期限を設定してみましょう。

    @Test
    public void expired() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        long timeout = 1 * 1000; // 1 sec

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .setExpiration(new Date(System.currentTimeMillis() + timeout))
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

        try {
            TimeUnit.SECONDS.sleep(2L);
        } catch (InterruptedException e) {
            // ignore
        }

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString)
        )
                .isInstanceOf(ExpiredJwtException.class)
                .hasMessageContaining("JWT expired at");
    }

有効期限は、Dateで指定します。今回は、有効期限を1秒にしてみます。

        long timeout = 1 * 1000; // 1 sec

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .setExpiration(new Date(System.currentTimeMillis() + timeout))
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

こうすると、JWTを作成した直後はパースできますが

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

少し待つと、有効期限切れで例外がスローされます。

        try {
            TimeUnit.SECONDS.sleep(2L);
        } catch (InterruptedException e) {
            // ignore
        }

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString)
        )
                .isInstanceOf(ExpiredJwtException.class)
                .hasMessageContaining("JWT expired at");

生成されたJWTと、パース結果はこちら。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoiLCJleHAiOjE1NDIyOTQ0NzJ9.jaio2KTNJ-OQdMJ-qe9mhOv6LFrvA8PKEz6rvW10Tx0
parsed JWT = header={alg=HS256},body={sub=磯野カツオ, exp=1542294472},signature=jaio2KTNJ-OQdMJ-qe9mhOv6LFrvA8PKEz6rvW10Tx0
署名をちょっといじってみる

JWT(というかJWS)を構成している、最後の要素である署名を少しいじって、検証でエラーになることを確認してみます。

    @Test
    public void invalidKey() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString.substring(0, jwsString.length() - 1))
        )
                .isInstanceOf(SignatureException.class)
                .hasMessage("JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.");
    }

JWTを1文字削ったので、署名が合わなくなり例外がスローされました。

JWTクレーム・セットを設定する

最後に、JWTクレーム・セットを設定してみます。

以下のように、Claimsを使うことでMapのように(というか、Mapの実装になっているのですが)、任意の項目を設定
することができます。

    @Test
    public void setClaims() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

        String jwsString =
                Jwts.builder()
                        .setClaims(claims)
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getBody().getIssuer()).isEqualTo("foo");
        assertThat(parsedJws.getBody().get("mydata")).isEqualTo("Hello!!");

        assertThat(parsedJws.getBody().toString()).isEqualTo("{sub=磯野カツオ, iss=foo, mydata=Hello!!}");

        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

こんな感じですね。

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

        String jwsString =
                Jwts.builder()
                        .setClaims(claims)
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

もともと標準的に決まっているもの(Standard Claims)については、以下のようにメソッドが用意されていたりします。

        String jwsString =
                Jwts.builder()
                        .setSubject("...")
                        .setIssuer("...")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

Claimsに対しても、キーが定義されたりしてましたね。

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

getterもあるわけですよ。

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getBody().getIssuer()).isEqualTo("foo");

Mapのキーを、Builderに対して直接設定することもできるので、そのあたりはドキュメントを参照するとよいでしょう。

Claims

同様のことが、JWTヘッダーにも言えます。

Header Parameters

このサンプルで、生成されたJWTとパース結果は、こちらです。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoiLCJpc3MiOiJmb28iLCJteWRhdGEiOiJIZWxsbyEhIn0.r2Ew-mOe5G05x_1_Y6qM7FeTEpteev_Rc2YKNjAOOZM
parsed JWT = header={alg=HS256},body={sub=磯野カツオ, iss=foo, mydata=Hello!!},signature=r2Ew-mOe5G05x_1_Y6qM7FeTEpteev_Rc2YKNjAOOZM

なんとなく、雰囲気はつかめたので、これで良しとしましょう。