웹서비스에 소셜 로그인을 붙이려면 각 플랫폼의 OAuth 인증 방식을 따라야 한다. 카카오 로그인은 국내에서 가장 널리 쓰이는데, 많은 개발자들이 공식 SDK 없이 직접 cURL로 구현하다가 토큰 갱신, 에러 처리, 사용자 정보 파싱에서 헤맨다. 이번에는 Kakao Developers OAuth 2.0 인증 흐름, PHP에서의 정확한 구현법, 실전 예제까지 완벽하게 정리해서 소개하겠다.
카카오 로그인은 표준 OAuth 2.0 인증 코드 흐름(Authorization Code Flow)을 따른다. 사용자가 '카카오로 로그인' 버튼을 클릭하면, 카카오 인증 서버로 리다이렉트되고, 승인 후 Authorization Code를 받아서 이를 Access Token으로 교환하는 방식이다. Access Token으로 사용자 정보를 조회할 수 있고, 토큰 만료 시 Refresh Token으로 갱신한다.
카카오 공식 문서에서는 JavaScript SDK나 REST API 직접 호출을 권장하는데, PHP 백엔드에서 토큰 관리를 해야 한다면 cURL 기반 구현이 필수다. 특히 세션 저장, 토큰 보안, 에러 처리 부분에서 실수하기 쉬우니 주의해야 한다.
먼저 Kakao Developers에 접속해서 앱을 만들어야 한다. '내 애플리케이션'에서 새 앱을 만들 때 앱 이름만 입력하면 되고, 앱이 생성되면 '앱 설정 > 일반'에서 REST API 키를 확인할 수 있다. 이 키가 인증 과정에서 클라이언트 식별자로 쓰인다.
다음으로 '제품 설정 > 카카오 로그인'에 들어가서 '활성화'를 켜고, 'Redirect URI'를 등록해야 한다. 로컬 테스트 환경에서는 http://localhost:8000/oauth/callback.php 같은 식으로, 운영 환경에서는 https://yourdomain.com/oauth/callback.php처럼 설정한다. 이 URI는 카카오가 Authorization Code를 돌려보낼 주소다. URI를 정확히 등록하지 않으면 '유효하지 않은 redirect_uri'라는 에러가 뜬다.
'동의 항목' 탭에서 필요한 사용자 정보(닉네임, 이메일, 프로필 사진 등)를 선택하면 된다. 선택한 항목만 조회 권한을 갖는다.
사용자가 클릭할 '카카오로 로그인' 버튼을 만들려면 인증 서버의 Authorization Endpoint로 리다이렉트하는 URL을 생성해야 한다.
<?php
// login_button.php
session_start();
$rest_api_key = 'YOUR_REST_API_KEY'; // Kakao Developers에서 발급받은 REST API 키
$redirect_uri = 'http://localhost:8000/oauth/callback.php'; // 등록된 Redirect URI
// Authorization Request URL 생성
$auth_url = 'https://kauth.kakao.com/oauth/authorize?';
$auth_url .= 'client_id=' . urlencode($rest_api_key);
$auth_url .= '&redirect_uri=' . urlencode($redirect_uri);
$auth_url .= '&response_type=code';
$auth_url .= '&state=' . bin2hex(random_bytes(16)); // CSRF 방지
// 생성된 상태 토큰을 세션에 저장 (콜백에서 검증)
$_SESSION['oauth_state'] = substr($auth_url, strrpos($auth_url, '&state=') + 8);
?>
<a href="<?= htmlspecialchars($auth_url); ?>">
카카오로 로그인
</a>
✓ 위 코드의 핵심은 state 파라미터를 생성해서 세션에 저장하는 부분이다. 이는 CSRF 공격 방지를 위해 필수다. 나중에 콜백에서 받은 state 값과 비교해서 일치하지 않으면 요청을 거부한다.
카카오 인증 서버는 사용자가 승인하면 code 파라미터와 state 파라미터를 가지고 Redirect URI로 리다이렉트한다. PHP에서 이 code를 받아서 Access Token으로 교환해야 한다.
<?php
// oauth/callback.php
session_start();
$rest_api_key = 'YOUR_REST_API_KEY';
$admin_key = 'YOUR_ADMIN_KEY'; // (선택) 사용자 정보 조회에 사용
$redirect_uri = 'http://localhost:8000/oauth/callback.php';
// CSRF 검증: state 파라미터 일치 확인
if (!isset($_GET['state']) || $_GET['state'] !== $_SESSION['oauth_state']) {
die('CSRF 공격 감지: state 파라미터 불일치');
}
// Authorization Code 받기
if (!isset($_GET['code'])) {
die('Authorization Code 수신 실패');
}
$code = $_GET['code'];
// Access Token 요청
$token_url = 'https://kauth.kakao.com/oauth/token';
$post_data = [
'grant_type' => 'authorization_code',
'client_id' => $rest_api_key,
'redirect_uri' => $redirect_uri,
'code' => $code
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $token_url,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($post_data),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
die('Access Token 요청 실패: HTTP ' . $http_code);
}
$token_data = json_decode($response, true);
if (!isset($token_data['access_token'])) {
die('Access Token 파싱 실패: ' . print_r($token_data, true));
}
// Access Token 저장 (세션 또는 DB)
$_SESSION['kakao_access_token'] = $token_data['access_token'];
$_SESSION['kakao_refresh_token'] = $token_data['refresh_token'] ?? null;
$_SESSION['kakao_token_expires_in'] = $token_data['expires_in'];
echo '<pre>';
echo 'Access Token: ' . $token_data['access_token'] . '\n';
echo 'Refresh Token: ' . ($token_data['refresh_token'] ?? 'N/A') . '\n';
echo 'Expires In: ' . $token_data['expires_in'] . 's\n';
echo '</pre>';
?>
✓ 주의점: HTTP 상태 코드를 반드시 확인해야 한다. 잘못된 code나 redirect_uri로 요청하면 400 에러가 돌아온다. 또한 refresh_token은 항상 반환되지 않을 수 있으니(첫 요청 시에만) 존재 여부를 확인하는 게 좋다.
Access Token을 받았으면 이제 사용자의 카카오 계정 정보를 조회할 수 있다. 카카오 API는 사용자 정보 조회(GET /v2/user/me)와 로그아웃(POST /v2/user/logout) 등 여러 엔드포인트를 제공한다.
<?php
// user_info.php
session_start();
if (!isset($_SESSION['kakao_access_token'])) {
die('로그인 필요');
}
$access_token = $_SESSION['kakao_access_token'];
// 사용자 정보 조회
$user_url = 'https://kapi.kakao.com/v2/user/me';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $user_url,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
die('cURL 에러: ' . $curl_error);
}
if ($http_code === 401) {
// 토큰 만료 또는 유효하지 않음
die('Access Token 무효 또는 만료');
}
if ($http_code !== 200) {
die('사용자 정보 조회 실패: HTTP ' . $http_code);
}
$user_data = json_decode($response, true);
echo '<pre>';
echo 'Kakao ID: ' . $user_data['id'] . '\n';
echo 'Nickname: ' . ($user_data['properties']['nickname'] ?? 'N/A') . '\n';
echo 'Profile Image: ' . ($user_data['properties']['profile_image'] ?? 'N/A') . '\n';
echo 'Email: ' . ($user_data['kakao_account']['email'] ?? 'N/A') . '\n';
echo '</pre>';
?>
✓ Authorization 헤더에 'Bearer ' + 토큰을 붙여야 한다. 토큰이 유효하지 않으면 401 에러가 돌아온다. 이 경우 Refresh Token이 있다면 새 Access Token을 요청해야 한다.
Access Token은 기본적으로 6시간 후 만료된다. Refresh Token이 있으면 이를 이용해 새 토큰을 받을 수 있다.
<?php
// refresh_token.php
session_start();
$rest_api_key = 'YOUR_REST_API_KEY';
$refresh_token = $_SESSION['kakao_refresh_token'] ?? null;
if (!$refresh_token) {
die('Refresh Token 없음. 다시 로그인해주세요.');
}
$token_url = 'https://kauth.kakao.com/oauth/token';
$post_data = [
'grant_type' => 'refresh_token',
'client_id' => $rest_api_key,
'refresh_token' => $refresh_token
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $token_url,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($post_data),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
die('Refresh Token 요청 실패. 다시 로그인해주세요.');
}
$token_data = json_decode($response, true);
// 새 토큰 저장
$_SESSION['kakao_access_token'] = $token_data['access_token'];
if (isset($token_data['refresh_token'])) {
$_SESSION['kakao_refresh_token'] = $token_data['refresh_token'];
}
echo '새 Access Token 발급 완료';
?>
✓ Refresh Token도 시간이 지나면 만료될 수 있으니, 새로 발급받은 refresh_token이 있으면 업데이트하는 것이 좋다. 토큰 갱신 실패 시 사용자에게 다시 로그인하도록 안내해야 한다.
사용자가 로그아웃하면 카카오 쪽 토큰도 무효화해야 한다. 그렇지 않으면 같은 기기에서 다른 사용자가 로그인했을 때 이전 토큰이 계속 유효할 수 있다.
<?php
// logout.php
session_start();
$rest_api_key = 'YOUR_REST_API_KEY';
$access_token = $_SESSION['kakao_access_token'] ?? null;
if ($access_token) {
// 카카오 서버에 로그아웃 요청
$logout_url = 'https://kapi.kakao.com/v2/user/logout';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $logout_url,
CURLOPT_POST => 1,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
echo '카카오 로그아웃 성공';
}
}
// 세션 정리
unset($_SESSION['kakao_access_token']);
unset($_SESSION['kakao_refresh_token']);
unset($_SESSION['oauth_state']);
session_destroy();
echo '로그아웃 완료';
?>
✓ 로그아웃 API 호출 실패해도 로컬 세션은 반드시 정리해야 한다. 네트워크 오류 등으로 카카오 요청이 실패하더라도 로컬 토큰이 없으면 비인증 상태가 되기 때문이다.
| 문제 | 원인 | 해결책 |
|---|---|---|
| 유효하지 않은 redirect_uri 에러 | 콜백 URL이 Kakao Developers에 등록된 URI와 정확히 일치하지 않음 | 프로토콜(http/https), 도메인, 포트, 경로까지 정확히 일치시킬 것 |
| state 파라미터 불일치 | CSRF 검증 로직 누락 또는 state 생성/저장 오류 | 인증 요청 시 state를 생성해서 세션에 저장, 콜백에서 비교 |
| 401 Unauthorized 응답 | Access Token 무효 또는 만료, Authorization 헤더 형식 오류 | 토큰 유효성 확인, Refresh Token으로 갱신, 또는 다시 로그인 |
| 사용자 정보가 비어있음 | '동의 항목' 설정에서 필요한 정보를 선택하지 않음 | Kakao Developers에서 동의 항목을 활성화하고 사용자에게 재동의 요청 |
| refresh_token이 없음 | 첫 로그인 시에는 refresh_token이 반환되지 않을 수 있음 | refresh_token이 없으면 주기적으로 로그인 새로고침 유도 |
위의 단편들을 합쳐서 완전한 로그인 시스템을 만들면 다음과 같다.
<?php
// config.php - 상수 정의
define('KAKAO_REST_API_KEY', 'YOUR_REST_API_KEY');
define('KAKAO_REDIRECT_URI', 'http://localhost:8000/oauth/callback.php');
define('KAKAO_AUTH_URL', 'https://kauth.kakao.com/oauth/authorize');
define('KAKAO_TOKEN_URL', 'https://kauth.kakao.com/oauth/token');
define('KAKAO_USER_URL', 'https://kapi.kakao.com/v2/user/me');
?>
<?php
// KakaoOAuth.php - 클래스로 모듈화
class KakaoOAuth {
private $rest_api_key;
private $redirect_uri;
public function __construct($rest_api_key, $redirect_uri) {
$this->rest_api_key = $rest_api_key;
$this->redirect_uri = $redirect_uri;
}
public function getAuthUrl() {
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;
$params = [
'client_id' => $this->rest_api_key,
'redirect_uri' => $this->redirect_uri,
'response_type' => 'code',
'state' => $state
];
return 'https://kauth.kakao.com/oauth/authorize?' . http_build_query($params);
}
public function getAccessToken($code) {
$post_data = [
'grant_type' => 'authorization_code',
'client_id' => $this->rest_api_key,
'redirect_uri' => $this->redirect_uri,
'code' => $code
];
$response = $this->curlPost('https://kauth.kakao.com/oauth/token', $post_data);
return $response['access_token'] ?? null;
}
public function getUserInfo($access_token) {
return $this->curlGet('https://kapi.kakao.com/v2/user/me', $access_token);
}
private function curlPost($url, $data) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
throw new Exception('HTTP ' . $http_code . ': ' . $response);
}
return json_decode($response, true);
}
private function curlGet($url, $access_token) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
],
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
throw new Exception('HTTP ' . $http_code . ': ' . $response);
}
return json_decode($response, true);
}
}
?>
카카오 OAuth 2.0 인증은 기본 흐름은 단순하지만, 토큰 갱신, CSRF 검증, 에러 처리까지 고려하면 구현할 게 많다. 특히 state 파라미터로 CSRF를 방어하고, HTTP 상태 코드를 꼼꼼히 확인하는 습관이 중요하다. 이 글의 예제 코드를 그대로 따라하고, KakaoOAuth 클래스처럼 모듈화해서 재사용하면, 안정적인 소셜 로그인 시스템을 빠르게 구축할 수 있을 것이다. 실전 환경에서는 토큰을 데이터베이스에 암호화해서 저장하고, 만료 시간을 추적해서 주기적으로 갱신하는 로직도 추가하면 더욱 견고해진다.