웹 애플리케이션에서 소셜 로그인과 JWT(JSON Web Token)를 활용한 안전한 인증 방식에 대해 알아보겠습니다.
특히 백엔드 중심의 소셜 로그인 구현 방식을 쉽게 설명해 드리겠습니다.
소셜 로그인은 Google, Kakao, Facebook 등의 소셜 미디어 계정을 이용해 다른 웹사이트나 애플리케이션에 로그인하는 방법입니다.
사용자는 새로운 계정을 만들지 않아도 되고, 서비스 제공자는 사용자 인증을 소셜 플랫폼에 위임할 수 있어 편리합니다.
JWT(JSON Web Token)는 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준입니다.
세 부분으로 구성됩니다:
HTTP-Only 쿠키는 JavaScript를 통해 접근할 수 없는 특수한 쿠키입니다. document.cookie로 읽거나 수정할 수 없으며, 오직 HTTP 요청을 통해서만 서버로 전송됩니다. 이 특성은 XSS(Cross-Site Scripting) 공격으로부터 쿠키를 보호하는 중요한 보안 장치입니다.
"Reissue"는 "재발급"을 의미합니다. JWT 인증에서는 보안을 위해 액세스 토큰의 유효기간을 짧게 설정합니다. 액세스 토큰이 만료되면 새로운 토큰을 발급받아야 하는데, 이 과정을 "토큰 재발급(Token Reissue)"이라고 합니다.
우리 구현에서 /reissue 경로는 리프레시 토큰을 이용해 새로운 액세스 토큰을 발급받는 중간 단계를 담당합니다. 소셜 로그인 후 리프레시 토큰만 받은 상태에서 액세스 토큰을 요청하는 과정이라고 볼 수 있습니다.
이제 전체 인증 흐름을 단계별로 자세히 살펴보겠습니다:
사용자가 웹사이트에서 "구글로 로그인" 또는 "카카오로 로그인" 버튼을 클릭합니다. 이 버튼은 일반적인 AJAX 요청이 아닌 하이퍼링크 형태로 구현되어 있습니다.
// 예시 코드
function handleGoogleLogin() {
window.location.href = 'http://api.myservice.com/login/google';
}
사용자가 백엔드 URL로 이동하면, 백엔드 서버는 다음과 같은 작업을 수행합니다:
이 과정은 모두 백엔드에서 처리되므로 프론트엔드에서는 별도의 OAuth 라이브러리나 설정이 필요 없습니다.
백엔드는 소셜 플랫폼에서 받은 사용자 정보를 확인하고 인증 토큰을 생성합니다:
Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5...; HttpOnly; Secure; Path=/; Max-Age=604800/reissue)로 사용자를 리다이렉트HTTP-Only 쿠키를 사용하는 이유는 JavaScript로 접근할 수 없게 하여 XSS 공격으로부터 보호하기 위함입니다.
사용자가 /reissue 페이지에 도착하면, 자동으로 백엔드 API를 호출하여 액세스 토큰을 요청합니다:
// /reissue 페이지의 코드
useEffect(() => {
async function getAccessToken() {
try {
const response = await axios.get('http://api.myservice.com/api/jwt/access-token', {
withCredentials: true // 쿠키를 포함해서 요청
});
// 응답 처리
if (response.headers.authorization) {
const accessToken = response.headers.authorization.replace('Bearer ', '');
// 액세스 토큰 저장
localStorage.setItem('accessToken', accessToken);
// 메인 페이지로 이동
window.location.href = '/main';
}
} catch (error) {
console.error('액세스 토큰 요청 실패', error);
window.location.href = '/login'; // 오류 시 로그인 페이지로 이동
}
}
getAccessToken();
}, []);
여기서 withCredentials: true 설정은 CORS 요청에서 쿠키를 포함시키기 위한 중요한 옵션입니다.
백엔드는 요청에 포함된 리프레시 토큰을 검증하고 액세스 토큰을 발급합니다:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...프론트엔드는 응답 헤더에서 액세스 토큰을 추출하여 저장하고, 이후 API 요청 시 이 토큰을 사용합니다:
// API 요청 예시
async function fetchUserData() {
const accessToken = localStorage.getItem('accessToken');
const response = await axios.get('http://api.myservice.com/api/user/profile', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return response.data;
}
이 인증 방식의 보안상 장점은 다음과 같습니다:
/reissue 경로의 역할/reissue 경로는 소셜 로그인 후 백엔드에서 프론트엔드로 리다이렉트되는 중간 지점입니다. 이 페이지의 주요 역할은:
이 과정이 "재발급(reissue)"이라 불리는 이유는, 리프레시 토큰을 사용해 액세스 토큰을 새로 발급받는 과정이기 때문입니다. 소셜 로그인 직후에는 리프레시 토큰만 있고 액세스 토큰은 없는 상태이므로, 이 과정을 통해 첫 액세스 토큰을 발급받게 됩니다.
CORS 설정: 백엔드에서 적절한 CORS 설정이 필요합니다.
Access-Control-Allow-Origin: https://yourfrontend.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization
쿠키 설정: 보안을 위해 SameSite, Secure 옵션도 설정하는 것이 좋습니다.
Set-Cookie: refresh_token=token; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800
에러 처리: 토큰 만료, 인증 실패 등 다양한 경우에 대한 처리가 필요합니다.
백엔드 중심의 소셜 로그인과 JWT를 활용한 인증 방식은 프론트엔드의 복잡성을 줄이면서도 높은 보안성을 제공합니다. 리프레시 토큰은 HTTP-Only 쿠키로 안전하게 보관하고, 액세스 토큰은 짧은 유효기간을 가지고 있어 보안 위험을 최소화합니다.
이 방식은 특히 다양한 소셜 로그인을 지원해야 하는 서비스에서 프론트엔드 코드의 중복을 줄이고, OAuth 관련 설정과 보안 이슈를 백엔드에서 집중적으로 관리할 수 있게 해줍니다.
웹 애플리케이션을 개발할 때 인증 시스템은 가장 중요한 부분 중 하나입니다. 이 글에서 설명한 방식을 참고하여 안전하고 사용자 친화적인 인증 시스템을 구현하시기 바랍니다!