Base64・URL エンコード・JWT の解説
データをテキストとして送る場面、つまり URL、HTTP ヘッダー、JSON、ログイントークンには、この 3 つがいつも登場します。しかし、どれもデータを隠すものではありません。このガイドでは、それぞれの仕組み、出会う場所、そして多くのトラブルの原因になる間違いを説明します。
エンコードは暗号化ではない
エンコードは、特定の文字しか通さない経路をデータが通れるように、書き方を変えることです。鍵を使わないので、誰でも元に戻せます。暗号化もデータを変えますが、元に戻せるのは鍵を持つ人だけです。ハッシュはさらに別物で、元に戻せない一方向の指紋です。詳しくはハッシュ関数の解説をご覧ください。
Base64 と URL エンコードはエンコードです。JWT は通常署名されていて、誰が発行したかと内容が変更されていないことを証明しますが、内容そのものはエンコードされているだけです。値を秘密にする必要があるなら、これらの形式ではなく暗号化が必要です。
Base64 の仕組み
Base64 はデータを 3 バイト(24 ビット)ずつ読み、6 ビットずつ 4 つのグループに分けます。各グループは 0〜63 の数で、64 文字のアルファベットの 1 文字として書きます。0〜25 は A〜Z、26〜51 は a〜z、52〜61 は 0〜9、その後が + と / です。「Man」という単語で手順を見てみましょう。
| 手順 | M | a | n |
|---|---|---|---|
| バイト値 | 77 | 97 | 110 |
| ビット | 01001101 | 01100001 | 01101110 |
| 6 ビットのグループ 4 つ | 010011 · 010110 · 000101 · 101110 = 19 · 22 · 5 · 46 | ||
| Base64 | T W F u → TWFu | ||
入力の長さが 3 の倍数でないときは、最後のグループを埋めてパディングで示します。1 バイト余れば 2 文字と ==、2 バイト余れば 3 文字と = になります。「Ma」は TWE=、「M」は TQ== です。
3 バイトが 4 文字になるため、Base64 はデータを約 3 分の 1 大きくし、形式によっては改行も加わります。メールに添付した 3 MB の写真は約 4 MB のテキストとして送られ、スタイルシートにデータ URL で埋め込んだ画像は元のファイルより約 3 分の 1 重くなります。
Base64 に出会う場所
- メールの添付ファイル(MIME)は、1 行 76 文字で折り返した Base64 テキストです。
- データ URL は
data:image/png;base64,iVBORw0…のように、小さなファイルを HTML や CSS に直接埋め込みます。 - HTTP 基本認証は
user:passwordを Base64 にして Authorization ヘッダーで送ります。パスワードを一見して読めなくするだけなので、安全なのは HTTPS 上だけです。 - 証明書と鍵の PEM ファイルは、
-----BEGIN …-----と-----END …-----の行の間にある、64 文字で折り返した Base64 です。 - JSON と XML にはバイナリ型がないため、署名やサムネイルなどのバイナリ値を Base64 文字列で持ちます。
Base64URL とパディング
標準 Base64 の + と / は URL やファイルパスで意味を持つため、2 つ目のアルファベットである Base64URL は代わりに - と _ を使います。デコーダーが長さを計算できるので、= のパディングも通常は省きます。JWT、多くの API トークン、一部のファイル名が Base64URL を使います。標準 Base64 を Base64URL にするには、+ を -、/ を _ に置き換え、末尾の = を消します。元に戻すときは置き換えを逆にし、長さが 4 の倍数になるまで = を付け足します。
URL のパーセントエンコード
URL に使える文字は限られていて、そのうちいくつかはアドレスの構造を表します。/ と ? はパスとクエリを分け、& と = はパラメーターを区切り、# はフラグメントの始まりです。値にこれらの文字や ASCII 以外の文字が含まれる場合はパーセントエンコードします。値を UTF-8 にした各バイトを、% の後に 16 進数 2 桁で書く方法です。é は C3 A9 の 2 バイトなので、「café」は caf%C3%A9 になります。
英字、数字、そして - . _ ~ はエンコードの必要がありません。トラブルの原因になりやすい文字は次のとおりです。
| 文字 | エンコード後 | 問題になる理由 |
|---|---|---|
| スペース | %20 | URL に使えない |
| & | %26 | 新しいパラメーターが始まる |
| = | %3D | パラメーター名が終わる |
| ? | %3F | クエリが始まる |
| # | %23 | フラグメントが始まり、その部分はサーバーに送られない |
| / | %2F | パスの階層が増える |
| + | %2B | フォームデータではスペースとして読まれる |
| % | %25 | エスケープシーケンスが始まる |
値を 1 つずつエンコードしてから URL を組み立てましょう。JavaScript の encodeURIComponent は値 1 つ用で、/ ? & = # もエンコードします。一方 encodeURI はアドレス全体用なので、これらの文字をそのまま残します。値 1 つに encodeURI を使うのは、クエリ文字列が壊れるよくある原因です。
プラス記号、スペース、二重エンコード
HTML フォームはフィールドを application/x-www-form-urlencoded 形式で送り、スペースは + になります。一般的な URL の構文では、+ はただのプラス記号です。そのため「C++」の検索を q=C++ と送ると、フォームデータをデコードするサーバーには「C」の後にスペース 2 つが付いた値として届きます。正しくは q=C%2B%2B と送ります。クエリ文字列を手でデコードするときは、どの規則で作られたかを先に確かめてください。
二重エンコードは、すでにエンコードされた値をもう一度エンコードすると起こります。% 自体が %25 になるので、%20 は %2520 になります。URL に %25 の後に 16 進数 2 桁が続く部分があれば、処理のどこかで 2 回エンコードしています。一度デコードして、余分な手順がどこにあるかを探してください。
JWT の中身
JSON Web Token(JWT)は、ユーザーが誰か、トークンがいつまで有効かといったクレームを、発行するシステムから受け入れるシステムへ運びます。よく使われる署名付きの形式は、ドットで区切られた Base64URL の 3 つの部分、つまりヘッダー、ペイロード、署名からなります。前の 2 つをデコードすると普通の JSON です。
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user-42", "role": "editor", "iat": 1767225600, "exp": 1767229200}
標準(RFC 7519)はいくつかの登録クレームを定めており、アプリケーションは上の role のように独自のクレームを追加します。
| クレーム | 意味 |
|---|---|
| iss | 発行者:トークンを作った側 |
| sub | 主体:通常はユーザーやアカウントの ID |
| aud | 対象:トークンを使うサービス |
| exp, nbf, iat | 有効期限、有効開始、発行時刻:秒単位の Unix 時間 |
| jti | トークン ID:再利用の検出や 1 つのトークンの失効に使う |
ペイロードはエンコードされているだけなので、トークンを持つ人なら誰でも読めます。個人情報や秘密は入れず、トークンは小さく保ちましょう。トークンはリクエストのたびに送られ、多くのサーバーはリクエストヘッダーのサイズを制限しています。
署名は秘密ではない:JWT の検証方法
署名はヘッダーとペイロードをもとに計算します。HS256 では、発行者と受信者が 1 つの秘密鍵を共有し、SHA-256 による HMAC を使います。RS256 や ES256 では、発行者が秘密鍵で署名し、受信者は対になる公開鍵で確認するため、受信者が自分でトークンを作ることはできません。
トークンを受け取るサーバーは、どのクレームを信用するにも、その前に想定したアルゴリズムと鍵で署名を検証し、exp、nbf、iss、aud を確認する必要があります。LeedyBox のツールのようにデコードするだけでは、これらをすべて省いています。トークンに何が書かれているかはわかりますが、本物かどうかはわかりません。ライブラリは、署名なしのトークンを表す「none」アルゴリズムも拒否しなければなりません。署名なしのトークンを使うと決めている場合を除きます。内容そのものを隠す必要があるときはトークンを暗号化(JWE)し、その場合トークンは 5 つの部分からなり、鍵がなければ読めません。
よくある問題と対処法
| 症状 | 考えられる原因 | 対処法 |
|---|---|---|
| Base64 をデコードすると文字化けする | バイナリデータ、または別のエンコードのテキスト | 何をエンコードしたかを確認し、バイナリデータはファイルとして保存する |
| デコード時に「不正な文字」エラー | data: の接頭辞、引用符、または Base64URL のアルファベット | カンマより前をすべて消し、- と _ を受け付けるデコーダーを使う |
| パラメーターが途中で切れる | 値にエンコードされていない & や # | 値ごとに encodeURIComponent でエンコードする |
| URL に %2520 がある | 値を 2 回エンコードした | URL を組み立てる箇所で 1 回だけエンコードする |
| exp が未来なのにトークンが拒否される | 対象や発行者の不一致、時計のずれ、鍵の不一致 | aud、iss、署名鍵をサーバーが期待する値と比べる |
| 有効期限が数千年先になる | exp を秒ではなくミリ秒で書いた | exp、nbf、iat を秒単位で保存する |
よくある質問
Base64 でパスワードや API キーを守れますか?
いいえ。Base64 は誰でも一瞬で元に戻せます。設定ファイルや URL に Base64 で入っている認証情報は、平文と同じくらい丸見えです。
JWT のペイロードに個人情報を入れてもよいですか?
避けましょう。ブラウザー拡張機能、ログ、リクエストを見るプロキシなど、トークンを手にした側は誰でもペイロードを読めます。トークンには ID だけを入れ、詳しい情報はサーバーで参照してください。
URL エンコードはインジェクション攻撃を防ぎますか?
値が URL の構造を壊せないという意味でのみ有効です。サーバーは、デコードした値を SQL、HTML、シェルコマンドなど実際に使う場所に合わせて検証し、エスケープする必要があります。
Base64 の文字列が元のファイルより長いのはなぜですか?
3 バイトが 4 文字になるため、テキストは約 33% 大きくなり、改行がさらに少し加わります。大きなファイルは Base64 にするより、バイトのまま送るほうが効率的です。