PHP에서 JWT 다루기 (인코딩·디코딩·서명 검증)

조회수 31

PHP로 JWT(JSON Web Token)를 만들고 검증하는 코드를 정리한 내용이다. 외부 라이브러리 없이 base64·서명·DER 변환까지 직접 구현한 형태이며, 구조와 호출 순서를 정리하였다.

1. JWT 구조 한 줄로

JWT는 헤더.페이로드.서명 세 부분을 점(.)으로 이은 문자열이다. 헤더·페이로드는 JSON을 URL-safe base64로 인코딩하고, 서명은 그 둘을 이어 붙인 것을 비밀키(또는 개인키)로 서명한 뒤 다시 base64 인코딩한 것이다.

flowchart LR A["header
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_decodecode가 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

  • 첫 번째 댓글을 남겨보세요.