최신 RESTful API 서버는 세션(Session) 대신 상태를 저장하지 않는(Stateless) JWT 인증 방식을 주로 채택합니다. 서버에서 비밀 키(Secret Key)로 서명하여 발급한 토큰을 프론트엔드가 보관하고, 매 API 요청마다 헤더에 담아 전송하여 자신을 증명하는 인증 프로세스를 Slim 프레임워크에 구현해 봅니다.
1. JWT(JSON Web Token)의 3단 구조
JWT는 .(점)을 기준으로 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 나뉩니다. 누구나 Base64 디코딩으로 내용을 열어볼 수 있으므로 비밀번호 같은 민감한 정보는 절대 넣으면 안 되며, 마지막 서명(Signature) 부분이 위조 방지의 핵심 역할을 합니다.
2. 패키지 설치 및 로그인 시 토큰 발급 (Encode)
가장 검증된 패키지인 firebase/php-jwt 를 컴포저로 설치합니다. 클라이언트가 올바른 계정 정보를 보내면, 서버는 식별자(user_id)와 만료 시간(exp)을 담아 토큰을 생성(Encode)하여 반환합니다.
routes.php (Auth)
<?php
use Firebase\JWT\JWT;
$app->post('/api/login', function ($request, $response) {
$data = $request->getParsedBody();
// (생략) DB에서 유저 조회 및 password_verify() 검증 수행
// if (!$isValidUser) { return jsonResponse($response, ['error'=>'실패'], 401); }
$secretKey = $_ENV['JWT_SECRET']; // ⚠️ 절대 하드코딩 금지, .env에서 로드
$issuedAt = time();
$expire = $issuedAt + 3600; // 1시간 후 만료
// Payload 데이터 구성
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'sub' => 7 // 인증된 유저의 Primary Key
];
// JWT 라이브러리를 통해 암호화(Encode)된 토큰 문자열 생성
$token = JWT::encode($payload, $secretKey, 'HS256');
return jsonResponse($response, ['token' => $token], 200);
});
?>
3. 보호된 API 접근 시 토큰 검증 (Decode)
프론트엔드는 발급받은 토큰을 로컬 스토리지에 보관하다가, 보안이 필요한 API를 호출할 때마다 Authorization: Bearer [토큰] 헤더에 담아 보냅니다. 서버는 이를 해독(Decode)하여 위변조 및 만료 여부를 검사합니다.
routes.php (Profile API)
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$app->get('/api/profile', function ($request, $response) {
$header = $request->getHeaderLine('Authorization');
$token = str_replace('Bearer ', '', $header); // Bearer 키워드 분리
if (empty($token)) {
return jsonResponse($response, ['error' => '토큰이 누락되었습니다.'], 401);
}
try {
$secretKey = $_ENV['JWT_SECRET'];
// 💡 토큰 해독 시도: 만약 위조되었거나 만료되었다면 여기서 즉시 Exception 발생!
$decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
$userId = $decoded->sub; // 토큰에 담아뒀던 유저 ID 꺼내기
// (DB 조회 등 비즈니스 로직 수행)
return jsonResponse($response, ['user_id' => $userId, 'name' => 'John'], 200);
} catch (\Exception $e) {
// 해독 실패 (만료, 변조 등)
return jsonResponse($response, ['error' => '유효하지 않은 토큰입니다.'], 401);
}
});
?>
Backend Router Execution Log
[Info] GET /api/profile 요청 수신
[Info] Authorization: Bearer eyJhbGciOiJ... 헤더 파싱
> JWT 토큰 서명(Signature) 유효성 검증 중...
[Success] 검증 성공! 서버가 발급한 유효한 토큰입니다.
[Info] Payload 해독 -> user_id: 7 추출
[Success] 7번 유저 프로필 데이터 반환 (HTTP 200)
[Info] GET /api/profile (위조된 요청) 수신
[Info] Authorization: Bearer eyJhbGciOiJ... 헤더 파싱
> JWT 토큰 서명(Signature) 유효성 검증 중...
[Error] ⛔ 서명 불일치! 누군가 Payload를 변조했습니다.
[Error] 접근 거부 (HTTP 401 Unauthorized)
⚠️ 시크릿 키 관리 및 보안 (매우 중요)
토큰의 서명(Signature)을 검증할 때 사용하는 Secret Key가 외부에 유출되면, 해커가 임의로 서명을 새로 만들 수 있으므로 시스템 보안이 완전히 붕괴됩니다. 이 키는 절대 코드 저장소(Git)에 올리지 말고 서버의 .env 환경 변수로 엄격하게 관리해야 합니다.
JWT 인증 흐름 검증
위의 백엔드 실행 로그 목업에서 볼 수 있듯, 정상적인 토큰이 들어오면 서명 검증 후 안전하게 유저 식별자(user_id)를 추출해 API를 제공합니다. 반면, 누군가 Payload를 변조하여 다른 권한으로 상승하려 해도, Secret Key가 없는 한 올바른 서명(Signature)을 새로 만들 수 없으므로 JWT::decode() 함수에서 즉각적으로 예외(Exception)를 발생시키고 401 Unauthorized 에러를 뱉어내며 철벽 방어를 수행합니다.