파일 업로드
  1. base64hub
  2. Base64 가이드
  3. Base64와 Base64URL
가이드

Base64와 Base64URL: 알파벳, 패딩, 함정

Base64URL은 출력을 URL이나 파일 이름에 그대로 넣을 수 있도록 두 문자를 바꾼 Base64입니다. 바이트는 똑같고, 알파벳이 두 군데에서 다릅니다.

표 하나로 보는 차이

두 변형 모두 RFC 4648에 정의되어 있습니다. 4절은 표준 Base64를, 5절은 “URL and Filename safe” 알파벳을 설명합니다. 62개 문자와 알고리즘은 같고, 인덱스 62와 63만 다릅니다.

표준 Base64 (RFC 4648 4절)Base64URL (RFC 4648 5절)
인덱스 0–61A–Z a–z 0–9같음
인덱스 62+-
인덱스 63/_
패딩기본적으로 = 필수보통 생략
URL 경로나 쿼리에 안전아니요예
파일 이름으로 안전아니요 (/는 경로 구분자)예

같은 입력을 두 방식으로 인코딩하면 다음과 같습니다.

입력표준Base64URLBase64URL, 패딩 없음
<<???>>PDw/Pz8+Pg==PDw_Pz8-Pg==PDw_Pz8-Pg
바이트 fb ff+/8=-_8=-_8
ManTWFuTWFuTWFu

마지막 행에서 보듯 많은 문자열은 두 변형에서 똑같아 보입니다. +, /, -, _가 들어 있거나 패딩이 빠져 있을 때만 구분할 수 있습니다. 구분할 수 없다면 두 알파벳을 모두 받는 디코더를 쓰는 것이 안전합니다.

받은 문자열을 빠르게 알아보는 방법은 다음과 같습니다.

  • -나 _가 있습니다: Base64URL.
  • +나 /가 있습니다: 표준 Base64.
  • 길이가 4의 배수가 아니고 =가 없습니다: 패딩이 없는 것이며, 거의 언제나 Base64URL입니다.
  • +와 -가 함께 있거나 /와 _가 함께 있습니다: 무언가가 알파벳을 섞었거나 데이터가 아예 Base64가 아닙니다. Base64 검사기가 어떤 문자가 잘못되었는지 보여 줍니다.
  • 점으로 나뉜 세 부분이 eyJ로 시작합니다: 정의상 Base64URL인 JWT입니다.

Base64URL이 존재하는 이유

URL에서 /는 경로 구간을 나누고, +는 폼 인코딩된 쿼리 문자열에서 공백을 뜻하며, =는 키와 값을 나눕니다. 표준 Base64는 이 세 문자를 모두 씁니다. URL에 넣으려면 퍼센트 인코딩을 해야 하므로 ab+/8=가 ab%2B%2F8%3D가 되어, 더 길고, 보기 싫고, 틀릴 수 있는 단계가 하나 더 생깁니다.

RFC 4648 5절은 URL에서 예약되지 않았고 흔히 쓰는 모든 파일 시스템에서 파일 이름으로 쓸 수 있는 -와 _를 골라 이를 해결합니다. RFC는 길이를 알 수 있다면 패딩을 생략해도 된다는 점도 언급하는데, = 자체도 %3D로 이스케이프해야 하기 때문입니다. 자세한 내용은 패딩 가이드에 있습니다.

각 변형이 쓰이는 곳

표준 Base64:

  • MIME 이메일 첨부 파일과 인코딩된 헤더(이메일 속 Base64)
  • PEM 인증서와 키
  • HTML과 CSS의 data: URI
  • HTTP Basic Auth 헤더
  • 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)
  • Python의 secrets.token_urlsafe() 같은 무작위 URL 토큰. 무작위 32바이트에 대해 43자 문자열을 돌려줬습니다

서로 변환하기

변환은 순수한 문자 치환이며 패딩만 따로 처리합니다. 바이트는 절대 바뀌지 않습니다.

표준에서 URL-safe로: +를 -로, /를 _로 바꾸고 끝의 =를 지웁니다. URL-safe에서 표준으로: -를 +로, _를 /로 바꾸고 길이가 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 인코더패딩
Pythonbase64.urlsafe_b64encode(b)유지, .rstrip(b"=")로 제거
Node.jsbuf.toString("base64url")생략
Gobase64.RawURLEncoding / base64.URLEncoding생략 / 유지
JavaBase64.getUrlEncoder().withoutPadding()생략
PHPrtrim(strtr(base64_encode($s), '+/', '-_'), '=')생략

디코딩할 때 Node의 Buffer.from(s, "base64")는 두 알파벳과 빠진 패딩을 모두 받습니다. Python의 base64.urlsafe_b64decode는 먼저 패딩을 복원해야 합니다. 브라우저의 atob()는 -와 _를 InvalidCharacterError로 거부하므로, 먼저 변환하거나 지원되는 환경에서는 alphabet: "base64url"과 함께 Uint8Array.fromBase64를 쓰세요. 다른 언어의 코드는 코드 페이지에 있으며, Python과 Java도 있습니다.

문자열 하나만 바꾸면 된다면 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-safe 입력에는 urlsafe_b64decode를 쓰거나, validate=True로 불일치를 드러내세요.

이중 인코딩

이 이름으로 불리는 서로 다른 두 가지가 있고, 둘 다 실제로 일어납니다.

퍼센트 인코딩을 두 번. 표준 Base64 문자열을 코드에서 한 번, HTTP 클라이언트가 또 한 번 이스케이프하면 +가 %2B가 되었다가 %252B가 됩니다. 서버는 한 번만 디코딩하므로 문자 그대로의 %2B를 받습니다. Base64URL은 이스케이프할 것이 없으므로 이 문제가 없습니다.

Base64의 Base64. 애플리케이션이 한 번, 프레임워크나 Kubernetes 매니페스트 도구가 또 한 번처럼 값이 두 층에서 인코딩됩니다. Man은 TWFu가 되고, 다시 VFdGdQ==가 됩니다. 디코딩한 결과가 여전히 Base64 문자로만 되어 있다면 이 경우입니다. 한 번 더 디코딩해서 결과가 말이 되는지 확인하세요.

패딩에 대한 기대

패딩을 요구하는 수신 측도 있고 거부하는 수신 측도 있습니다. Go의 RawURLEncoding은 =가 든 입력에서 실패하고, Python의 디코더는 =가 없는 입력에서 실패합니다. API를 정의할 때는 문서에 변형과 패딩 규칙을 명시하고, 자신의 디코더는 두 가지를 모두 받게 만드세요.

Base64URL 디코딩

패딩이 있든 없든 URL-safe 문자열을 붙여넣고 안에 무엇이 있는지 확인하세요.

Base64URL 디코더

자주 묻는 질문

Base64URL은 Base64 문자열을 URL 인코딩한 것과 같은가요?

아니요. 퍼센트 인코딩은 +를 %2B로, /를 %2F로 바꾸므로 문자열이 길어집니다. Base64URL은 다른 알파벳을 써서 이스케이프가 아예 필요 없습니다.

표준 Base64 디코더로 Base64URL을 읽을 수 있나요?

확실하지 않습니다. Node의 Buffer처럼 두 알파벳을 모두 받는 디코더도 있습니다. -와 _를 거부하는 디코더도 있고, Python의 b64decode는 기본적으로 이를 조용히 버려서 틀린 바이트를 만듭니다.

Base64URL에는 패딩이 있나요?

RFC 4648은 허용하고, Python의 urlsafe_b64encode도 패딩을 유지합니다. JWT를 비롯해 Base64URL을 쓰는 대부분의 형식은 길이로 이미 알 수 있으므로 =를 뺍니다.

URL 속 토큰에는 무엇을 써야 하나요?

패딩 없는 Base64URL입니다. 경로, 쿼리 문자열, 쿠키, 파일 이름 어디서도 이스케이프가 필요 없습니다.

계속 읽기