導入
たきの「博士〜!」
たきの「遊園地に来たんですが、この紙を見せるだけでアトラクションに乗れるんですよ!」
博士「ほう、それは入園パスじゃな。」
たきの「一回チケット売り場で本人確認をしただけなのに、その後は何回見せても大丈夫なんですね。」
博士「ふむ。」
博士「実はWebサービスにも、それとよく似た仕組みがあるんじゃ。」
たきの「Webサイトにも入園パスがあるんですか?」
博士「それが**JWT(JSON Web Token)**じゃ。」
JWT
博士「JWT(JSON Web Token)とは、ログインしたユーザーであることを証明するためのトークンじゃ。」
たきの「トークン?」
博士「簡単に言えばデジタル会員証のようなものじゃ。」
例えば、一度ログインすると、
ユーザー
│
│ ログイン
▼
サーバー
│
│ JWTを発行
▼
eyJhbGciOiJIUzI1NiIs...
その後は、
GET /mypage
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
のようにJWTを送るだけで、
「この人はログイン済みだ」
と判断できる。
たきの「なるほど!」
博士「さらにJWTには、認証だけでなく、認可に必要な情報も入れられるんじゃ。」
たきの「博士..。認証、認可ってなんでしたっけ?」
認証
博士「認証とは、あなたは誰ですか? を確認することじゃ。」
例えば、
ID:takino
Password:1234
を入力すると、
サーバー
「あ、この人はたきのさんだ」
と本人確認を行う。
つまり、
認証 = 本人確認
である。
たきの「遊園地でいうと、チケット売り場で本人確認している場面なんですね!」
博士「その通り。」
博士「そして本人確認が終わると、JWTという入園パスが発行される。」
たきの「なるほど!」
たきの「じゃあJWTを持っていたら、どこへでも入れるんですか?」
博士「いや、それは違う。」
博士「そこで出てくるのが認可じゃ。」
認可
博士「認可とは、その人が何をしてよいか決めることじゃ。」
例えば、
一般ユーザー
なら
- 商品を見る
- 商品を買う
だけできる。
一方、
管理者
なら
- 商品追加
- 商品削除
- ユーザー削除
などもできる。
つまり、
認証
↓
本人確認
認可
↓
権限確認
という違いになる。
たきの「なるほど!」
たきの「会員証を持っていても、入れない部屋があるような感じですね!」
博士「その通りじゃ。」
博士「そして、その権限もJWTに入れられるんじゃ。」
JWTでの認証・認可
博士「例えばJWTにはこんな情報が入っておる。」
{
"sub": "takino",
"role": "ADMIN"
}
サーバーは、
role = ADMIN
を見て、
管理画面OK
と判断する。
逆に、
role = USER
なら、
403 Forbidden
となる。
博士「つまりJWTには、
- この人は誰なのか(認証)
- この人は何ができるのか(認可)
の情報が入っているんじゃ。」
たきの「なるほど!」
たきの「JWTは**eyJhbGciOiJIUzI1NiIs...**みたいな形でしたよね」
たきの「中身ってどうなっているんですか?」
JWTの中身
JWTは3つの部分からできている。
xxxxx.yyyyy.zzzzz
| 部分 | 内容 |
|---|---|
| Header | 使用する署名アルゴリズム |
| Payload | ユーザー情報・権限・有効期限 |
| Signature | 改ざん防止の署名 |
実際のJWTは次のような形じゃ。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJ0YWtpbm8iLCJyb2xlIjoiQURNSU4iLCJleHAiOjE3NTAwMDAwMDB9
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
例えばPayloadには、
{
"sub":"takino",
"role":"ADMIN",
"exp":1750000000
}
のような情報が入る。
Payloadは暗号化ではなく、Base64でエンコードされているだけです。
パスワードなどの機密情報は保存してはいけません。
たきの「なるほど!」
Session認証との比較
博士「JWTと一番比較されるのがSession認証じゃ。」
| 項目 | Session認証 | JWT |
|---|---|---|
| ログイン情報の保存場所 | サーバー | クライアント |
| 通信時に送るもの | Session ID | JWT |
| サーバー側の管理 | セッションを管理する必要がある | セッション管理が不要 |
| サーバーを増やした場合 | セッション共有が必要 | トークンを検証するだけでよい |
| 向いている場面 | 従来のWebサイト | Web API・SPA・スマホアプリ |
博士「ReactやFlutterでは、サーバー側でSessionを管理しなくてよいJWTがよく使われる。」
たきの「なるほど!」
たきの「JWT,session以外にも認証方法ってあるんですか?」
博士「もちろんじゃ。」
他にもよく使われる認証・認可との比較
| 方式 | 何をする仕組み? | 例 |
|---|---|---|
| Session認証 | サーバーでログイン状態を管理する | 一般的なWebサイト |
| JWT | トークンを使ってログイン状態をやり取りする | Web API |
| OAuth 2.0 | 他サービスに権限を渡す | Google Drive連携 |
| OpenID Connect | 他サービスでログインする | Googleログイン |
たきの「認証と認可、それにJWTとの違いも分かってきました!」
博士「ふむ。」
博士「では、JWT認証が実際にどのように動いているのか見ていこう。」
JWT認証の流れ
まずはログインする。
クライアント
(ブラウザ・アプリ)
│
ID・Password
▼
サーバー
│
認証
│
JWTを発行
▼
クライアント
このとき、
- ユーザーがIDとパスワードを入力する
- サーバーが本人確認(認証)を行う
- 認証に成功するとJWTを発行する
- クライアントがJWTを保存する
たきの「ここまでは普通のログインと同じなんですね。」
博士「そうじゃ。」
博士「違うのは、この後じゃ。」
APIへアクセスするときは、
クライアント
│
Authorization: Bearer JWT
▼
サーバー
│
JWTを検証
│
ログイン済みと判断
▼
API実行
つまり、
- クライアントはJWTを付けてAPIへアクセスする
- サーバーはJWTが正しいか確認する
- 問題がなければログイン済みとしてAPIを実行する
博士「最初だけIDとパスワードを送り、その後はJWTだけを送るんじゃ。」
たきの「だから毎回ログインし直さなくてもいいんですね!」
博士「その通り。」
博士「では実際のコードを見てみよう。」
Spring Bootでの実装例
博士「JWT認証をSpring Bootで実装する流れを見てみよう。」
① ログインAPI
まずはログインAPIを作成する。
ユーザーが入力したID・パスワードを確認し、正しければJWTを発行する。
@PostMapping("/login")
public String login(@RequestBody LoginRequest request) {
// ユーザー名・パスワードが正しいか確認(認証)
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 認証成功後、JWTを発行
return jwtService.generateToken(request.getUsername());
}
ポイント
-
authenticate()で本人確認(認証)を行う - 認証に成功するとJWTを発行する
- JWTをクライアントへ返す
② JWTを生成
JWTにはユーザー情報や有効期限などを設定する。
public String generateToken(String username) {
return Jwts.builder()
// ユーザー名を保存
.subject(username)
// 発行日時
.issuedAt(new Date())
// 有効期限(1時間)
.expiration(new Date(System.currentTimeMillis() + 3600000))
// 改ざん防止の署名
.signWith(secretKey)
// JWTを生成
.compact();
}
ポイント
このJWTには、
- ユーザー名
- 発行日時
- 有効期限
- 署名
が保存される。
③ JWTを送信
ログイン後は、APIへアクセスするたびにJWTをHTTPヘッダーへ付けて送信する。
fetch("/api/users", {
headers: {
// JWTを付与
Authorization: `Bearer ${token}`
}
});
実際にはこのようなHTTPリクエストになる。
GET /api/users
Authorization: Bearer eyJhbGc...
ポイント
サーバーは、このJWTを見てログイン済みか判断する。
④ JWTを取得
サーバーは、送られてきたJWTをAuthorizationヘッダーから取り出す。
// Authorizationヘッダーを取得
String authHeader = request.getHeader("Authorization");
// Bearerから始まる場合だけ取得
if (authHeader != null &&
authHeader.startsWith("Bearer ")) {
// JWTだけ取り出す
String jwt = authHeader.substring(7);
}
ポイント
Bearer eyJhbGc...
↑
JWT
Bearerを取り除いた文字列がJWTである。
⑤ JWTを検証
最後に、JWTが改ざんされていないか、有効期限が切れていないかを確認する。
// JWTからユーザー名を取得
String username = jwtService.extractUsername(jwt);
// 改ざん・期限切れを確認
if (jwtService.isTokenValid(jwt)) {
// Spring Securityへログイン済みとして登録
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
username,
null,
List.of());
SecurityContextHolder.getContext()
.setAuthentication(auth);
}
ポイント
JWTが正しいと判断されると、
- ログイン済み
- ユーザー情報を取得できる
- 認可(権限チェック)ができる
ようになる。
処理の流れ
API受信
│
Authorizationヘッダー取得
│
JWTを取り出す
│
JWTを検証
│
ログイン済みと判断
│
API実行
まとめ
博士「JWTを一言で表すと…」
ログイン済みを証明するデジタル会員証
JWTを利用すると、
- サーバー側でSession管理が不要
- ReactやFlutterとの相性が良い
- REST APIで広く利用されている
- 認証・認可の情報をトークンでやり取りできる
というメリットがある。
たきの「遊園地の入園パスで考えると、JWTが何をしているのかイメージしやすかったです!」
博士「ふむ。」
博士「では、今日の一句」
今日の一句
トークンを 取っとくん?と 問うトークン