PHP로 JWT(JSON Web Token)를 만들고 검증하는 코드를 정리한 내용이다. 외부 라이브러리 없이 base64·서명·DER 변환까지 직접 구현한 형태이며, 구조와 호출 순서를 정리하였다.
1. JWT 구조 한 줄로
JWT는 헤더.페이로드.서명 세 부분을 점(.)으로 이은 문자열이다. 헤더·페이로드는 JSON을 URL-safe base64로 인코딩하고, 서명은 그 둘을 이어 붙인 것을 비밀키(또는 개인키)로 서명한 뒤 다시 base64 인코딩한 것이다.
typ, alg"] B["payload
body"] C["signature"] D["base64url"] E["JWT 문자열"] A --> D B --> D D --> C C --> E
2. 사용되는 함수 흐름
정리한 코드에서는 대략 다음과 같이 나뉜다.
- common_base64_encode / common_base64_decode — URL-safe base64 (+, / → -, _; 패딩 = 제거)
- common_der_encode / common_der_decode — DER 형식 인코딩·디코딩 (ES256 서명 변환 시 사용)
- common_signature_from_der / common_signature_to_der — OpenSSL이 주는 DER 서명 ↔ JWT에서 쓰는 r|s 형태 변환
- common_create_sign — 메시지 + 키 + 알고리즘으로 서명 생성 (HS256 / ES256 / RS256)
- common_verify_sign — 메시지 + 서명 + 키로 검증
- common_jwt_encode — body 배열 + key + alg(기본 hs256)로 JWT 문자열 생성
- common_jwt_decode — JWT 문자열 + key(또는 kid별 키 배열)로 파싱·검증 후 head/body 반환
3. 내장 함수로 가능한지 검토
PHP에는 jwt_encode 같은 JWT 전용 내장 함수가 없다. 따라서 커스텀(common_*) 함수를 사용하며, ES256·RS256·kid까지 쓰려면 DER 변환이나 OpenSSL 리소스 처리 때문에 해당 구현을 유지하는 것이 맞다.
다만 HS256만 쓸 때는 json_encode, base64_encode(URL-safe는 str_replace), hash_hmac만으로 인코딩·검증이 가능하다. 아래는 내장 함수만 사용해 목적을 달성하는 예시이다.
HS256 — 내장 함수만으로 인코딩·검증 예시
// URL-safe base64 (내장: base64_encode + str_replace)
function base64url_encode($s) {
return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
}
function base64url_decode($s) {
return base64_decode(strtr($s, '-_', '+/'));
}
// HS256 JWT 생성 (내장: json_encode, hash_hmac)
function jwt_hs256_encode($body, $key) {
$header = json_encode(["typ" => "JWT", "alg" => "HS256"]);
$payload = json_encode($body);
$data = base64url_encode($header) . '.' . base64url_encode($payload);
$sig = hash_hmac('sha256', $data, $key, true);
return $data . '.' . base64url_encode($sig);
}
// HS256 JWT 검증 (내장: explode, base64url_decode, json_decode, hash_hmac)
function jwt_hs256_verify($jwt, $key) {
$parts = explode('.', $jwt);
if (count($parts) !== 3) return false;
$data = $parts[0] . '.' . $parts[1];
$sig = base64url_decode($parts[2]);
$expected = hash_hmac('sha256', $data, $key, true);
return hash_equals($expected, $sig);
}
실제로는 time(), json_encode, hash_hmac, hash_equals 같은 내장 함수만으로 HS256 목적을 달성할 수 있다. ES256·RS256이 필요하면 커스텀(common_*) 또는 외부 라이브러리를 사용해야 한다.
4. 프로젝트 공통 함수로 인코딩 (common_jwt_encode)
ES256·RS256·kid까지 쓰는 프로젝트에서는 아래처럼 common_jwt_encode를 사용한다.
$body = ["user_id" => 123, "exp" => time() + 3600];
$key = "your-secret-key";
$jwt = $ukp->common_jwt_encode($body, $key, "hs256");
알고리즘은 hs256, es256, rs256을 지원한다. ES256·RS256은 키가 OpenSSL 리소스(개인키/공개키)여야 한다.
5. JWT 디코딩 (토큰 검증·파싱)
점으로 split해서 세그먼트가 3개가 아니면 실패, head·body base64 디코딩·JSON 디코딩, alg 확인, kid가 있으면 키 배열에서 해당 키를 꺼내 쓰고, exp가 있으면 만료 여부를 확인한 뒤 서명 검증을 수행한다.
$result = $ukp->common_jwt_decode($jwt, $key);
if ($result["code"] != "1") {
// 실패: $result["code_msg"] 로 이유 확인
// code 2: 유효하지 않은 JWT, 3~5: head/body/sig 형식, 6: 알고리즘, 7~8: kid, 9: 만료, 10: 서명 실패
return;
}
$head = $result["head"];
$body = $result["body"];
키가 여러 개면 kid로 구분해 쓴다. $key = ["kid1" => "secret1", "kid2" => "secret2"]처럼 넘기면 헤더의 kid에 맞는 키로 검증한다.
6. 디코드 반환 코드 정리
common_jwt_decode의 code가 1이면 성공, 그 외는 실패 이유이다.
- 2 — 유효하지 않은 JWT (세그먼트 개수 등)
- 3 — head JSON/형식 오류
- 4 — body JSON/형식 오류
- 5 — signature base64/형식 오류
- 6 — 지원하지 않는 알고리즘
- 7 — kid 헤더 없음 (키가 배열인데)
- 8 — kid에 해당하는 키 없음
- 9 — 만료된 토큰 (exp < time())
- 10 — 서명 검증 실패
7. 자주 발생하는 문제와 대처
서명 검증 실패(code 10)가 나올 때
인코딩할 때 쓴 키와 디코딩할 때 넣는 키가 다르거나, 헤더·페이로드가 조금이라도 바뀌면 서명이 맞지 않는다. RS256이면 공개키로 검증해야 하는데 개인키를 넣으면 실패한다. 디코드할 때는 공개키를 넣어야 하며, 시크릿을 넣었다면 공개키로 바꾸면 해결된다.
만료된 토큰(code 9)만 계속 나올 때
body에 exp를 넣었다면 서버 시간이 맞는지 확인한다. 서버 시계가 느리면 아직 유효한데 만료로 처리될 수 있다.
ES256 사용 시 서명 오류
OpenSSL이 주는 DER 형식 서명을 JWT용 r|s 형태로 바꿔 주는 것이 common_signature_from_der / common_signature_to_der이다. 이 부분이 맞지 않으면 ES256 서명이 깨져 검증에 실패한다. ES256을 쓸 때는 DER 변환 로직을 그대로 사용하면 된다.
한 줄 요약
JWT는 header.payload.signature, URL-safe base64 + 서명. PHP에는 JWT 내장이 없어서 HS256만 쓰면 json_encode·base64_encode·hash_hmac으로 직접 구현 가능하고, ES256·RS256·kid를 쓰면 common_jwt_encode/decode 같은 공통 함수 사용. 디코드는 code 1이면 성공, 아니면 code_msg 확인. 서명 실패 시 키·alg 일치, exp 만료 시 서버 시간 확인.
댓글 0