違いを 1 枚の表で
どちらのバリアントも RFC 4648 で定義されています。4 章が標準の Base64、5 章が「URL and Filename safe」なアルファベットです。62 文字とアルゴリズムは共通で、違うのはインデックス 62 と 63 だけです。
| 標準 Base64(RFC 4648 §4) | Base64URL(RFC 4648 §5) | |
|---|---|---|
| インデックス 0–61 | A–Z a–z 0–9 | 同じ |
| インデックス 62 | + | - |
| インデックス 63 | / | _ |
| パディング | = がデフォルトで必須 | 通常は省略 |
| URL のパスやクエリで安全 | いいえ | はい |
| ファイル名として安全 | いいえ(/ はパスの区切り) | はい |
同じ入力を両方の方式でエンコードした例です。
| 入力 | 標準 | Base64URL | Base64URL(パディングなし) |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
バイト fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
最後の行が示すように、多くの文字列はどちらのバリアントでも同じに見えます。区別できるのは +、/、-、_ のいずれかを含むか、パディングが欠けている場合だけです。判別できないときは、両方のアルファベットを受け付けるデコーダーを使うのが安全です。
渡された文字列をすばやく見分ける方法:
-か_を含む:Base64URL。+か/を含む:標準 Base64。- 長さが 4 の倍数でなく、
=もない:パディングなしで、ほぼ間違いなく Base64URL。 +と-、または/と_の両方を含む:どこかでアルファベットが混ざったか、そもそも Base64 ではありません。Base64 検証ツールで、どの文字がおかしいかを確認できます。eyJで始まり、ドットで区切られた 3 つの部分がある:JWT で、定義上 Base64URL です。
Base64URL が存在する理由
URL では / がパスのセグメントを区切り、フォームエンコードされたクエリ文字列では + が空白を意味し、= がキーと値を区切ります。標準の Base64 はこの 3 つをすべて使います。そのまま URL に入れるにはパーセントエンコーディングが必要で、ab+/8= は ab%2B%2F8%3D になります。長く、読みにくく、間違えやすい手順が 1 つ増えます。
RFC 4648 §5 は、URL で予約されておらず、一般的などのファイルシステムでもファイル名に使える - と _ を選ぶことでこれを解決しました。また、= 自体も %3D とエスケープが必要になるため、長さがわかっている場合はパディングを省略してよいとも書かれています。詳しくはパディングの解説をご覧ください。
それぞれのバリアントが使われる場所
標準 Base64:
- MIME のメール添付ファイルとエンコードされたヘッダー(メールでの Base64)
- PEM の証明書と鍵
- HTML や CSS の
data:URI - HTTP Basic 認証のヘッダー
- Kubernetes の Secret や、ファイルを運ぶほとんどの JSON API
Base64URL(通常はパディングなし):
- JSON Web Token:ヘッダー、ペイロード、署名のすべてがパディングなしの Base64URL(JWT での Base64)
- JWK の鍵、JWS や JWE 全般(RFC 7515–7518)
- WebAuthn のクレデンシャル ID とチャレンジ
- OAuth PKCE の
code_challengeの値(RFC 7636) - ランダムな URL トークン。たとえば Python の
secrets.token_urlsafe()は、32 バイトの乱数から 43 文字の文字列を返しました
相互の変換
変換は単なる文字の置き換えで、パディングは別に扱います。バイト列は変わりません。
標準から URLセーフへ:+ を - に、/ を _ に置き換え、末尾の = を取り除きます。
URLセーフから標準へ:- を + に、_ を / に置き換え、長さが 4 の倍数になるまで = を足します。
const toUrl = (b64) =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromUrl = (u) => {
const b64 = u.replace(/-/g, "+").replace(/_/g, "/");
return b64 + "=".repeat((4 - (b64.length % 4)) % 4);
};
toUrl("PDw/Pz8+Pg=="); // "PDw_Pz8-Pg"
fromUrl("PDw_Pz8-Pg"); // "PDw/Pz8+Pg=="
ほとんどの言語には直接使えるエンコーダーもあるので、文字列操作が必要になることはまれです。
| 言語 | Base64URL エンコーダー | パディング |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | 残る。.rstrip(b"=") で除去 |
| Node.js | buf.toString("base64url") | 省略 |
| Go | base64.RawURLEncoding / base64.URLEncoding | 省略 / 残る |
| Java | Base64.getUrlEncoder().withoutPadding() | 省略 |
| PHP | rtrim(strtr(base64_encode($s), '+/', '-_'), '=') | 省略 |
デコードでは、Node の Buffer.from(s, "base64") は両方のアルファベットとパディングの欠落を受け付けます。Python の base64.urlsafe_b64decode は、先にパディングを戻す必要があります。ブラウザの atob() は - と _ を InvalidCharacterError で拒否するので、先に変換するか、使える環境なら alphabet: "base64url" を指定して Uint8Array.fromBase64 を使ってください。他の言語のコードはコード例のページにあります。たとえば Python や Java です。
文字列を 1 つ切り替えたいだけなら、Base64 から Base64URL へと Base64URL から Base64 へのツールがブラウザ内で変換します。
落とし穴
+ が空白に変わる
これが最もよくあるバグです。標準の Base64 をエスケープせずにクエリ文字列に入れると、+ を空白として読むフォームデコードによって壊されます。
from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="
その後のデコーダーは空白で失敗するか、空白を読み飛ばして誤ったバイト列を返します。対策は Base64URL を使うか、入れる側で encodeURIComponent() を通すことです。受け取る側での応急処置としては、Base64 に空白が含まれることはないので、空白を + に戻せばたいてい直ります。
アルファベットが黙って混ざる
Python の base64.b64decode は、validate=True を渡さない限り、標準アルファベット以外の文字を捨てます。Base64URL の文字列を渡すと、- と _ を落として残りをデコードします。
import base64
base64.b64decode("PDw_Pz8-Pg==")
# b'<<\x0f\xcf\xc3\xe0' (wrong; the input was "<<???>>")
base64.b64decode("PDw_Pz8-Pg==", validate=True)
# binascii.Error: Only base64 data is allowed
base64.urlsafe_b64decode("PDw_Pz8-Pg==")
# b'<<???>>'
最初の呼び出しはエラーを出さずに誤ったバイト列を返します。URLセーフな入力には urlsafe_b64decode を使うか、validate=True を指定して不一致をエラーとして表に出してください。
二重エンコード
この名前で呼ばれるものは 2 種類あり、どちらも実際に起こります。
パーセントエンコーディングを 2 回。 標準の Base64 文字列が自分のコードで 1 回、さらに HTTP クライアントでもう 1 回エスケープされ、+ が %2B、次に %252B になります。サーバーは 1 回だけデコードするので、受け取るのは文字どおりの %2B です。Base64URL ならエスケープするものがないので、この問題は起きません。
Base64 の Base64。 値が 2 つの層でエンコードされるケースです。たとえばアプリケーションでエンコードされ、さらにフレームワークや Kubernetes マニフェストのツールでもエンコードされます。Man が TWFu になり、次に VFdGdQ== になります。見分けるポイントは、デコードした結果がまだ Base64 の文字だけでできていることです。もう一度デコードして、意味のある結果になるか確かめてください。
パディングの扱いの違い
パディングを必須とする受け手もあれば、拒否する受け手もあります。Go の RawURLEncoding は = を含む入力で失敗し、Python のデコーダーは = が欠けた入力で失敗します。API を設計するときは、バリアントとパディングのルールをドキュメントに明記し、自分のデコーダーはどちらも受け付けるようにしてください。
URLセーフな文字列を、パディングの有無にかかわらず貼り付けて中身を確認できます。
よくある質問
Base64URL は Base64 文字列を URL エンコードしたものと同じですか?
いいえ。パーセントエンコーディングでは + が %2B、/ が %2F になり、文字列が長くなります。Base64URL は別のアルファベットを使うので、エスケープがまったく要りません。
標準の Base64 デコーダーで Base64URL を読めますか?
確実ではありません。Node の Buffer のように両方のアルファベットを受け付けるデコーダーもありますが、- と _ を拒否するものもあります。Python の b64decode はデフォルトでこれらを黙って捨てるため、誤ったバイト列になります。
Base64URL にパディングはありますか?
RFC 4648 は認めており、Python の urlsafe_b64encode もパディングを残します。ただし JWT を含め、Base64URL を使う形式の多くは、長さから決まるという理由で = を省略します。
URL に入れるトークンにはどちらを使うべきですか?
パディングなしの Base64URL です。パス、クエリ文字列、Cookie、ファイル名のどこでもエスケープが不要です。