✨ Intro
카카오 로그인은 카카오계정으로 다양한 서비스에 로그인할 수 있도록 하는 OAuth 2.0 기반의 소셜 로그인 서비스를 구현하려고 한다.
✨ 카카오 로그인 시퀀스 다이어그램
🔗 서비스 로그인 과정 - kakao developers
https://developers.kakao.com/docs/latest/ko/kakaologin/common
로그인은 REST API, JavaScript SDK, Android SDK 등 여러가지 방식으로 구현할 수 있는데 이 중 REST API를 사용했다.
✨ 로그인 단계 및 요청 정리
각 단계별 절차와 어디에 요청을 보내야하는지 정리 했다.
단계 | 설명 | 요청 |
1. 로그인 요청 | 사용자가 로그인 버튼 클릭 → 카카오 로그인 페이지 이동 | https://kauth.kakao.com/oauth/authorize |
2. 인가 코드 발급 | 로그인 후 code를 redirect_uri로 반환 | ?code=AUTHORIZATION_CODE |
3. 액세스 토큰 요청 | code를 사용해 access_token 발급 | https://kauth.kakao.com/oauth/token |
4. 사용자 정보 가져오기 | access_token으로 카카오 사용자 정보 조회 | https://kapi.kakao.com/v2/user/me |
5. 로그인 완료 | 사용자 정보 저장 & 전역 상태 관리 | - |
6. 로그아웃 | 토큰 삭제 및 상태 초기화 | https://kapi.kakao.com/v1/user/logout |
❓ 왜 인가 코드(Authorization Code) 발급 절차가 필요할까?
- "로그인 성공시 그냥 액세스 토큰을 바로 주면 안 되나?” 라는 질문이 떠오를 수 있다.
- 카카오 로그인 페이지에서 로그인이 성공하면 원래 서비스 페이지로 돌아오게 되는데 돌아오면서 토큰을 받으려면 아래와 같이 Redirect URL에 토큰을 넣을 수 밖에 없다.
- 이렇게 되면 브라우저 히스토리, 로그, 네트워크 요청에서 악성 코드가 토큰을 가로챌 수 있게 된다.
https://your-app.com/callback?access_token=eyJhbGciOiJIUzI1...
인가 코드 방식(Authorization Code Grant)
이러한 이유 때문에 OAuth 2.0에서는 보안 강화를 위해 "인가 코드"를 먼저 발급하고, 서버에서 토큰을 교환하는 방식을 사용한다.
OAuth 2.0
- 인증을 위한 개방형 표준 프로토콜이다.
- Third-Party 프로그램에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공한다.
- 구글, 페이스북, 카카오, 네이버 등에서 제공하는 간편 로그인 기능도 OAuth2 프로토콜 기반의 사용자 인증 기능을 제공한다.
💡 인가 코드 방식의 핵심 원리
- 로그인 요청하면, 클라이언트(브라우저)에게는 인가 코드(code)만 반환한다.
🔒 이 인가 코드는 액세스 토큰이 아니므로 인가 코드 자체로는 사용자 정보를 가져올 수 없다. (보안 강화)
https://your-app.com/callback?code=AUTHORIZATION_CODE
- 인가 코드를 서버에서 카카오에 보내고, 서버에서만 액세스 토큰을 받는다.
- 🔒 인가 코드는 특정 redirect_uri에서만 액세스 토큰으로 교환 가능하기 때문에 공격자가 인가 코드를 탈취해도 내 서버가 아닌 다른 곳에서는 사용할 수 없다. (토큰을 요청할 때 redirect_uri를 검증한다)
- 🔒 공격자가 임의의 사이트에서 code를 요청해도 액세스 토큰을 받을 수 없다.
POST https://kauth.kakao.com/oauth/token Body: { grant_type: "authorization_code", client_id: "YOUR_KAKAO_CLIENT_ID", redirect_uri: "YOUR_REDIRECT_URI", // ✅ URI 체크! code: "AUTHORIZATION_CODE" }
- 서버에서 받은 액세스 토큰을 클라이언트와 통신할 때만 사용한다.
즉, 액세스 토큰은 브라우저 URL에 노출되지 않아 보안 강화를 기대할 수 있다.
CSRF 토큰(State Parameter) 사용
❓ CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 토큰이란?
CSRF 토큰은 웹 애플리케이션에서 사용자의 세션을 보호하는 데 사용되는 무작위로 생성된 고유한 값이다.이 토큰은 사용자의 브라우저에 저장되며, 사용자가 웹 사이트에 요청을 보낼 때마다 서버에 전송된다.
- OAuth 2.0에서는 CSRF 공격을 방지하기 위해 클라이언트에서 state 파라미터를 포함하여 요청하는 방법을 제공한다.
- 요청시 보낸 state와 응답으로 받은 state가 일치하는지 검사하면 공격자가 요청을 위조할 가능성을 줄일 수 있다.
- 인가 코드 요청 시 프론트엔드에서 생성하고, 응답 받을 때 검증하는 방식으로 동작한다. 응답 받은 state(CSRF 토큰)가 다르면 CSRF 공격 가능성이 있는 요청으로 간주하고 차단한다.
https://kauth.kakao.com/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REDIRECT_URI
&response_type=code
&state=RANDOM_SECURE_STRING // ✅ CSRF 방지용 토큰
🔗 state 파라미터 사용 (CSRF 공격 방지) - kakao developers
https://developers.kakao.com/docs/latest/ko/getting-started/security-guideline#state-param
✨ state 요청 & 검증 예시 코드
state 값 생성 후 카카오 로그인 요청 URL에 포함
- state 값은 랜덤하게 생성한 후, localStorage에 저장해둔다.
- 카카오 로그인 페이지로 이동할 때 이 값을 state 파라미터로 포함해서 보낸다.
const generateRandomState = () => {
return Math.random().toString(36).substring(2, 15); // 랜덤 문자열 생성
};
const handleLogin = () => {
const state = generateRandomState();
localStorage.setItem("oauth_state", state); // ✅ state 값을 로컬에 저장
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize
?client_id=${KAKAO_CLIENT_ID}
&redirect_uri=${KAKAO_REDIRECT_URI}
&response_type=code
&state=${state}`; // ✅ state 추가
window.location.href = kakaoAuthUrl;
};
state 검증 (인가 코드 응답 시)
- 카카오가 인가 코드와 함께 state 값을 반환하면 localStorage에 저장해 둔 state 값과 같은지 검증한다.
- 만약 state 값이 다르면 CSRF 공격 가능성이 있으므로 요청을 차단한다.
const [searchParams] = useSearchParams();
const code = searchParams.get("code");
const receivedState = searchParams.get("state"); // ✅ 받은 state 값
useEffect(() => {
if (code && receivedState) {
const originalState = localStorage.getItem("oauth_state"); // ✅ 우리가 보낸 state 값
if (receivedState !== originalState) {
console.error("CSRF 공격 가능성 있음! 요청 차단");
return;
}
// ✅ 검증 완료 후, 인가 코드로 토큰 요청 진행
exchangeToken(code);
}
}, [code, receivedState]);
✨ 사용자 정보 획득하기
카카오에서 엑세스 토큰을 받았다면 로그인한 사용자의 이름, 이메일 등의 정보를 얻어올 수 있다.
🔗 사용자 정보 가져오기 - kakao developers
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
export const getUserInfo = async (access_token: string) => {
const response = await axios.get("https://kapi.kakao.com/v2/user/me", {
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/x-www-form-urlencoded",
},
});
return response.data;
};
- 기본적으로 사용자 id만 획득할 수 있는데 kakao developers 로그인 후 내 “애플리케이션 → 제품 설명 → 카카오 로그인 → 동의항목” 에서 설정할 수 있다.
- 다만 사업자정보를 등록하고 비지니스 인증을 완료 후 권한이 필요한 동의항목에 대한 심사를 신청할 수 있다.
✨ 카카오 로그인 구현하기
지금까지는 카카오 로그인 개념에 대한 설명이었으며 지금부터는 React 환경에서 구현하는 내용이다.
환경변수 등록하기
- 우선 kakao developers에서 Client ID와 Redirect URI를 등록 후 환경변수에 저장한다.
- vite로 프로젝트를 생성했기 때문에 REACT_APP_~이 아니라 VITE_~로 환경변수명을 지정해야 사용할 수 있다.
# .env
VITE_KAKAO_CLIENT_ID=[클라이언트ID]
VITE_KAKAO_REDIRECT_URI=[http://localhost:5174/auth/kakao]
- vite 프로젝트에서는 process.env.REACT_APP_~가 아니라 import.meta.env.VITE_~으로 환경변수를 가져올 수 있다.
- 다른파일에서 import.meta~이름이 긴게 싫어서 constants 파일에 상수를 선언해서 사용하려고 한다.
// src/config/constants.ts
export const KAKAO_CLIENT_ID = import.meta.env.VITE_KAKAO_CLIENT_ID;
export const KAKAO_REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI;
App.tsx
index페이지(path=”/”)에 로그인하기 버튼을 눌러 ID, PW을 입력하거나 소셜 로그인이 있는 로그인 페이지(path=”/login”)로 이동했다고 가정한다.
// src/App.tsx
import "./App.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./pages/Login";
import KakaoAuth from "./pages/KakaoAuth";
import UserInfo from "./components/auth/UserInfo";
const queryClient = new QueryClient();
function App() {
return (
<>
<QueryClientProvider client={queryClient}>
<Router>
<Routes>
<Route path="/" element={<UserInfo />} />
<Route path="/login" element={<Login />} />
<Route path="/auth/kakao" element={<KakaoAuth />} />
</Routes>
</Router>
</QueryClientProvider>
</>
);
}
export default App;
로그인 요청 & 인가 코드 발급 받기(Login.tsx)
카카오로 로그인하기 버튼 클릭
→ 카카오 로그인 페이지 이동
→ 로그인 성공시 환경변수에 미리 지정해 두었던 Redirect URI로 이동
- 카카오 로그인 페이지로 이동하는 url에 client_id와 redirect_uri를 넣어준다. response_type도 필수로 넣어줘야 하는데 code로 고정이다.
- 로그인 성공시 인가코드를 URI에 함께 전달 받는다.
// src/pages/Login.tsx
import { KAKAO_CLIENT_ID, KAKAO_REDIRECT_URI } from "../config/constants";
const Login = () => {
const handleKakaoLogin = () => {
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URI}&response_type=code`;
window.location.href = kakaoAuthUrl;
};
return (
<button
onClick={handleKakaoLogin}
style={{
backgroundColor: "#fee500",
padding: "10px 20px",
borderRadius: "8px",
border: "none",
cursor: "pointer",
}}
>
카카오 로그인
</button>
);
};
export default Login;
🔗 인가 코드 받기 - kakao developers
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code
KakaoAuth.tsx
- 카카오 ID와 PW를 입력후 성공시 카카오에서 “/auth/kakao” 페이지로 redirect 되도록 설정 했는데 searchParam에서 인가 코드를 확인할 수 있다.
- 이 페이지에서는 아래 절차들이 이루어진다.
- 인가코드를 활용하여 토큰을 발급 받기 → 전역 상태관리에 사용저 정보 저장 → 원래 서비스 페이지로 이동
// KakaoAuth.tsx
import { useEffect } from "react";
import { useSearchParams, useNavigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../stores/useAuthStore";
import { getUserInfo, useExchangToken } from "../services/login";
import { KAKAO_CLIENT_ID, KAKAO_REDIRECT_URI } from "../config/constants";
const KakaoAuth = () => {
const [searchParams] = useSearchParams();
const location = useLocation();
const authCode = searchParams.get("code");
const navigate = useNavigate();
const { setToken } = useAuthStore(); // Zustand Store
const { mutate, data } = useExchangToken();
useEffect(() => {
if (!authCode) return;
mutate(
{
code: authCode,
client_id: KAKAO_CLIENT_ID,
redirect_uri: KAKAO_REDIRECT_URI,
},
{
onSuccess: async (data) => {
setToken(data.access_token);
try {
const userInfoData = await getUserInfo(data.access_token);
// TODO 전역상태관리에 사용자 정보 저장하기
} catch (error) {
console.log(error);
} finally {
navigate("/");
}
},
onError: (error) => {
console.error("카카오 로그인 실패:", error);
navigate("/login");
},
}
);
}, [authCode]);
return <div>로그인 중...</div>;
};
export default KakaoAuth;
login.ts
카카오에 토큰을 받을때는 Post를 활용, 사용자 정보를 받아올 때는 Get을 사용해서 API를 요청한다.
// src/services/login.ts
import axios from "axios";
import { useMutation } from "@tanstack/react-query";
interface KakaoExchangeToken {
code: string;
client_id: string;
redirect_uri: string;
}
const kakaoExchangeToken = async ({
code,
client_id,
redirect_uri,
}: KakaoExchangeToken) => {
const response = await axios.post(
"https://kauth.kakao.com/oauth/token",
new URLSearchParams({
grant_type: "authorization_code",
client_id: client_id,
redirect_uri: redirect_uri,
code,
}),
{
headers: { "Content-Type": "application/x-www-form-urlencoded" },
}
);
return response.data;
};
export const useExchangToken = () => {
return useMutation({
mutationFn: kakaoExchangeToken,
});
};
export const getUserInfo = async (access_token: string) => {
const response = await axios.get("https://kapi.kakao.com/v2/user/me", {
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/x-www-form-urlencoded",
},
});
return response.data;
};
마무리
- 핵심 로직만을 최대한 간단히 표현하기 위해 state 값을 같이 보내는 로직은 구현하지 않았다.
- 아직 비지니스 사용자가 등록되지는 않아 사용자 정보 저장, 로그아웃 기능은 구현하지 않았지만 아래 문서를 참고하여 로그아웃을 구현하면 될 것 같다.
🔗 카카오계정과 함께 로그아웃 - kakao developers
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#logout-of-service-and-kakaoaccount
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
또한 소셜로그인 구현시 각 서비스마다 “로그인 버튼 사용 가이드”가 있는데 권장하는 크기와 비율 등을 잘 준수해야 한다.
🔗 [소셜 로그인 가이드라인] 네이버, 카카오, 구글, 애플
https://butsteadily.tistory.com/16
참고
'인증 & 보안' 카테고리의 다른 글
Checksum이란? (0) | 2023.12.11 |
---|---|
[인증 & 암호] 메세지 위변조를 확인하는 MAC & HAMC (0) | 2023.12.10 |