今回、JWTを用いた認証について学習したので、あわせてブログにまとめてみました。
本記事では、ログイン認証後のユーザーの各要求に対して、ログインユーザーと作業者が同一であるかをチェックするためにJWTを用いた処理を紹介します。
JWT(ジョット)
JWTとは、Json Web Tokenの略で、関係者間で情報を安全に送信するためのJsonオブジェクトです。
ディジタル署名されているため、検証及び信頼ができるトークンとなっています。
様々なアルゴリズムを使用した公開/秘密鍵のペアを使用して署名することができます。
用途
-
認証
ユーザーログイン成功時にトークンを生成します。
(シングルサインオンやセッション認証で使用) -
情報交換
公開鍵/秘密鍵のペアを用いて、コンテンツの改ざん等がされていないことを検証できます。
JWTの構造
JWTは、ヘッダ、ペイロード、サインで構成されており、それぞれ以下の情報を保持しています。
xxxxxxx(ヘッダ).yyyyyyy(ペイロード).zzzzzzz(サイン)
-
ヘッダ
トークンのタイプとアルゴリズムの2つの部分で構成されています。 -
ペイロード
-
JWTで用意されているよく使われるクレーム
発行者、有効期限、サブジェクト、オーディエンスを含む情報が格納されています。基本保持情報については、下記「JWTの基本保持情報」を参照ください。
※クレーム:各JSONデータの項目名のこと -
パブリッククレーム(上記以外の詳細なクレーム)
ユーザーが自由に定義できる情報が格納されています。詳細は、こちら。 -
プライベートクレーム(カスタムで作成できるクレーム)
プライベートな共有情報を含む情報が格納されています。
-
JWTで用意されているよく使われるクレーム
-
サイン
署名部分になります。詳細は こちら。
JWTの基本保持情報
id | name | description |
---|---|---|
jti | JWT ID | JWTの一意の識別子 |
aud | Audience | JWTの利用者 |
iss | Issuer | JWTの発行者 |
sub | Subject | JWTの主体.JWT発行者のユニークまたはグローバルな値 |
iat | Issued At | JWTの発行時間 |
nbf | Not Before | JWTの有効期間の開始時間.この時間より前には利用できません。 |
exp | Expiration Time | JWTの有効期間の終了時間. この時間より後には利用できません。 |
推奨
JWTは信頼できるトークンとするために、セキュリティ問題に細心の注意を払う必要があります。
例えば、以下のような点について気をつける必要があります。
- 長い間、保持することを進めません。
- ブラウザのセッションストレージに保存しないことが推奨されています。
JavaでのJWTの使用設定
JWTのライブラリは、各言語ごとに複数存在しており、以下のページで参照できます。
基本的には、ハッシュ値の計算に使用できるアルゴリズムの種類が異なったりしているようです。
ちなみに、今回は、「com.auth0 / java-jwt / 4.2.1」を使用しています。
環境
開発環境:Eclipse
フレームワーク:SpringBoot
使用言語:Java
Gradleの設定
build.gradleファイルを開き、dependenciesプロパティに以下を追加します。
implementation 'com.auth0:java-jwt:4.2.1'
JWTトークンの生成手順
それでは、認証で使用するトークンの生成について、以下に書いていきます。
今回は、SNSなどでログインユーザーと投稿者が、同一であるかの検証に用いるために、メールアドレスを一意の値として使用して、JWTの生成と検証を実装しています。
また、以下で作成するトークンは、あくまで、学習としてのものなので、推奨されているセキュリティ面を無視している部分がありますが、悪しからず。
メソッドの作成
今回、JWTでトークンを生成するコードは、以下になっています。
以降で、ひとつずつ処理を分解して説明していますので、さらっと見てください。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
public *class* JWTbuild {
private static final Long EXPIRATION_TIME = 1000L * 60L * 60L * 1L;
@Autowired
LoginItem loginItem;
private Hash secret = new Hash();
//セッショントークン生成するメソッド
public String build(String userEmail){
//生成のため、日時データを取得する
Date issuedAt = new Date();
Date notBefore = new Date(issuedAt.getTime());
Date expiresAt = new Date(issuedAt.getTime() + EXPIRATION_TIME);
String secretKey = secret.getSecretKey();
//ヘッダー部へのアルゴリズムとハッシュ値を指定する
Algorithm algorithm = Algorithm.HMAC256(secretKey);
//トークンの生成
String token = JWT.create()
.withIssuer(secret.getTokenIssuer()) //トークン発行者情報
.withSubject(secret.getTokenSubject()) //トークンの主体
.withAudience(userEmail) //トークンの利用者(メールアドレスを用いてトークンを一意にする)
.withIssuedAt(issuedAt) //発行日時
.withNotBefore(notBefore) //トークンの有効期間開始時間
.withExpiresAt(expiresAt) //トークンの有効期間終了時間 今回はログアウト、セッションタイムアウトまで保持
.sign(algorithm); //アルゴリズム指定して、署名を行う
return token;
}
}
ライブラリのインポート
JWT生成を行うクラスに以下のライブラリをインポートします。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
トークンの有効時間の取得
Dateクラスを用いて、日時の取得します。
発行日時、有効開始時間、有効終了時間をそれぞれ変数に代入します。
トークン生成時に、これらの変数を使用します。
private static final Long EXPIRATION_TIME = 1000L * 60L * 60L * 1L;
// ~~省略~~
//生成のため、日時データを取得する
Date issuedAt = new Date();
Date notBefore = new Date(issuedAt.getTime());
Date expiresAt = new Date(issuedAt.getTime() + EXPIRATION_TIME);
- 発行日時:トークンが発行された日時
- 有効期間開始時間:トークンが有効になる開始時間
- 有効期間終了時間:トークンが無効になる終了時間
今回、トークンの有効期限は生成時間から1時間、またはログアウトなどでトークンが破棄されるまでとしています。
署名生成のためのアルゴリズムの作成
次にアルゴリズムの生成に使用するシークレットな値を用意します。
今回、シークレットな値は、別のクラスに定義しています。(ファイル名:Hash.java)
以下では、Hashクラスの変数secretKeyの値をJWTbuildクラスの変数secretKeyに代入しています。
Hashクラスから取得した任意の文字列は、のちに生成するAlgorithmインスタンスクラスに保持されます。
また、Hashクラスの任意の文字列はトークンをデコード(復号)する際にも使用します。
private Hash secret = new Hash();
// ~~省略~~
String secretKey = secret.getSecretKey();`
public *class* Hash {
private static String secretKey = "lkhapsjf[jtpqu959qy34jlroqhefa"; //任意の文字列
private static String tokenIssuer = "your_name"; //発行者の名前
private static String tokenSubject = "service_token"; //任意のトークン名
public String getSecretKey() {
return secretKey;
}
public String getTokenIssuer() {
return tokenIssuer;
}
public String getTokenSubject() {
return tokenSubject;
}
}
つぎに、JWTライブラリ、Algorithmクラスを用いて計算方式を指定します。
引数として先ほどの変数secretKeyを渡します。
変数algorithmは後続処理のトークン生成時に署名のための引数として使用します。
Algorithm algorithm = Algorithm.HMAC256(secretKey);
なお、今回は計算方式としてSHA-256を使用したアルゴリズムのHMAC256を使用しています。
トークンの生成
JWTのcreateメソッドを用いて、トークンを生成します。
このとき、JWTクラスの各メソッドを用いて、さまざまな情報をトークンに保持させます。
~~省略~~
//トークンの生成
String token = JWT.create()
.withIssuer(secret.getTokenIssuer()) //トークン発行者情報(Hash.javaより取得)
.withSubject(secret.getTokenSubject()) //トークンの主体(Hash.javaより取得)
.withAudience(userEmail) //トークンの利用者(メールアドレスを用いてトークンを一意にする)
.withIssuedAt(issuedAt) //発行日時
.withNotBefore(notBefore) //トークンの有効期間開始時間
.withExpiresAt(expiresAt) //トークンの有効期間終了時間 今回はログアウト、セッションタイムアウトまで保持
.sign(algorithm); //アルゴリズム指定して、署名を行う
各メソッドについて
-
withIssuer トークンの発行者情報を保持します。
デコード(復号)するときにも必要な情報になるため、保管しておきましょう。
今回は、アルゴリズムの生成同様、Hashクラスから値を取得してます。 -
withSubject トークンの名前を保持します。
デコード(復号)するときにも必要な情報になるため、保管しておきましょう。
今回は、アルゴリズムの生成同様、Hashクラスから値を取得してます。 -
withAudience(任意) トークンの利用者を保持します。
上記は、SNSのセッショントークンを生成することを目的としているため、
ログインIDとして使用しているメールアドレスを変数userEmailに代入しておき、
引数として与えることで、トークンに一意性を持たせています。 -
withIssuedAt(任意) トークンの発行時間を保持します。
引数として、上記で作成した変数を渡します。 -
withNotBefore(任意) トークンの有効期間開始時間を保持します。
引数として、上記で作成した変数を渡します。 -
withExpiresAt(任意) トークンの有効期限終了時間を保持します。
引数として、上記で作成した変数を渡します。 - sign 署名を保持します。
- 引数として、上記で作成したアルゴリズムを渡します。
トークンをリターン
生成したトークンをリターンします。
今回、作成したメソッドをJWT生成時に呼び出すことで、JWTを生成することができます。
生成したJWTはフロントエンドに、一時的に保持させます。
各処理の要求時に、JWTをデコードしユーザー情報を確認することで、ログインユーザーと処理要求をした人が、同一人物であるかチェックするのに役立ちます。
それでは、デコードの方法についても見ていきましょう。
トークンの検証手順
生成したトークンを用いて、同じユーザーであるか検証していきます。
フロントエンドなどから届いたトークンをデコード(復号)して、ユーザー情報を取り出す方法を、以下に書いていきます。
メソッドの作成
今回、JWTでトークンをデコード(復号)するコードは、以下になります。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
public *class* JWTdecode {
private static final Long EXPIRATION_TIME = 1000L * 60L * 60L * 1L;
//セッショントークン情報に含まれたメールアドレスを取得する処理
public String decodeJwt(String token) {
DecodedJWT decodedJWT;
String secretKey = secret.getSecretKey();
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(secret.getTokenIssuer())
.withSubject(secret.getTokenSubject())
.build();
decodedJWT = verifier.verify(token);
List<String> userEmailList = decodedJWT.getAudience();
String userEmail = userEmailList.get(0);
return userEmail;
}
}
ライブラリのインポート
JWT復号を行うクラスに以下のライブラリをインポートします。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
アルゴリズムの作成
生成と同様の内容で、アルゴリズムを生成します。
このとき、生成とシークレットな値や計算方法を選択するとデコード(復号)できないため、同じ値を用いてアルゴリズムを生成します。
String secretKey = secret.getSecretKey();
Algorithm algorithm = Algorithm.HMAC256(secretKey);
トークンの検証内容の作成
トークン生成時に必須条件として保持させた情報とアルゴリズムを引数として渡し、検証内容を作成します。
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(secret.getTokenIssuer())
.withSubject(secret.getTokenSubject())
.build();
トークンの検証
トークンを引数として渡し検証します。 検証に問題がなければ、保持している情報を取り出すことができます。取り出した情報はリストの変数に代入します。
以下は、生成時に保持させたトークン利用者情報(ユーザーのメールアドレス)を取得しています。
decodedJWT = verifier.verify(token);
List<String> userEmailList = decodedJWT.getAudience();
後は、取り出した情報を利用しやすい型に変換して使用します。
String userEmail = userEmailList.get(0);
return userEmail;
例えば・・・
上記で取り出したメールアドレスを用いて、データベースからユーザー情報を取得する仕組みなどに用いることで、本人からの要求である信頼性が高くなります。
参考
https://github.com/auth0/java-jwt/blob/master/README.md
https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html
https://qiita.com/rubytomato@github/items/eb595303430b35f4773d