개발 일지/개발 일지

[개발 일지] JWT + 소셜 로그인을 정복해보자 (React + Spring Boot, Kakao Login, Naver Login)

2023. 4. 11. 18:00
목차
  1. 📚 1. 사전 지식
  2. JWT란?
  3. 소셜 로그인이란? oAuth?
  4. 소셜 로그인의 전체적인 흐름
  5. 🤩 2. 실제로 구현하기
  6. 1. 카카오/네이버로부터 인가코드 발급 받기
  7. 2. 발급받은 인가코드로 서버에 요청해서 토큰 발급받기
  8. 🤔 3. 느낀 점 / 배운 점 / 추가로 공부할 것

📚 1. 사전 지식

JWT란?

JWT(Json Web Token) : Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token

  • 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달
  • 주로 회원 인증이나 정보 전달에 사용되는 JWT는 아래의 로직을 따라서 처리

출처 : https://mangkyu.tistory.com/56

애플리케이션이 실행될 때, JWT를 static변수와 localStorage에 저장한다. static변수에 저장하는 이유는 HTTP통신을 할 때마다 JWT를 HTTP헤더에 담아서 보내야 하는데, localStorage에서 계속 불러오면 overhead가 발생하기 때문이다. 클라이언트에서 JWT를 포함해 요청을 보내면 서버는 허가된 JWT를 검사한다. 또한, 로그아웃을 할 경우 localStorage에 저장된 JWT를 제거한다.

 

소셜 로그인이란? oAuth?

소셜 로그인이란, 일명 간편 로그인으로 다른 서비스의 계정에 기대어 새로운 계정을 만들거나 계정에 접속하는 것이다. 서비스별로 아이디와 암호를 모두 외우는 대신 뿌리계정 하나의 아이디와 암호만 외우는 것이 서비스 이용자에게 훨씬 쉽기 때문에 빠르게 보급되었다.

많은 사이트에서 많은 이용자의 유입을 위해 소셜 로그인을 애용하고 있다. 우리 서비스에서는 소셜 로그인으로 카카오 로그인과 네이버 로그인을 도입하였다.

 

소셜 로그인의 전체적인 흐름

소셜 로그인 로직은 다음과 같다.

  1. auth/login, 소셜 로그인 버튼을 눌러 로그인 -> 카카오에 인가코드 발급 요청 -> 인가코드가 query string으로 담긴 주소로 redirect
  2. 주소의 query string(즉 인가코드)을 url에 담아 서버에 토큰을 요청
  3. 서버는 인가코드를 가지고 카카오에 토큰 발급 요청한 뒤 받은 토큰을 가지고 사용자 정보를 DB에 저장 -> 자체 jwt 토큰을 발급하여 클라이언트에 전송
  4. 서버로부터 받은 토큰(access, refresh)을 사용자의 localStorage에 저장

여기에서 인가코드 발급, 토큰 발급 두번의 요청에서 카카오의 경우 redirectURL, 네이버의 경우 STATESTRING(프런트에서 생성한 임의 변수)를 가지고 동일성 검사를 한다. 그렇다면, 실제 우리 서비스에서 코드와 같이 살펴볼까?


🤩 2. 실제로 구현하기

1. 카카오/네이버로부터 인가코드 발급 받기

먼저 소셜 로그인 버튼을 통해 인가코드 발급을 받도록 합니다. 버튼을 누르면 소셜 로그인 페이지로 이동하고 사용자가 동의할 경우 인가코드를 받게 됩니다. 인가코드는 redirect 된 url의 query string으로 전달받습니다.

프로젝트의 로그인 화면

// components/login/SocialLoginButton.tsx
export default function SocialLoginButton({
  kind,
  ...rest
}: SocialLoginButtonProps) {
  return kind === 'kakao' ? (
    <Link
      href={`https://kauth.kakao.com/oauth/authorize?client_id=${CONFIG.API_KEYS.KAKAO}&redirect_uri=${CONFIG.DOMAIN}/auth/kakao/callback&response_type=code`}
    >
      <KaKaoButton {...rest}>
        <Image src={kakao} width={20} height={20} alt="kakao" />
        <span className="ml-2 text-[#3B1E1E]">카카오톡으로 로그인</span>
      </KaKaoButton>
    </Link>
  ) : (
    <Link
      href={`https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${CONFIG.API_KEYS.NAVER}&state=${STATESTRING}&redirect_uri=${CONFIG.DOMAIN}/auth/naver/callback`}
    >
      <NaverButton {...rest}>
        <Image src={naver} width={20} height={20} alt="kakao" />
        <span className="ml-2 text-white">네이버로 로그인</span>
      </NaverButton>
    </Link>
  );
}

 

'카카오톡으로 로그인', 혹은 '네이버로 로그인' 버튼을 누르면 

위 사진과 같이 로그인 페이지로 이동이 됩니다. 

2. 발급받은 인가코드로 서버에 요청해서 토큰 발급받기

로그인을 하면 카카오/네이버에서 인가코드 발급하여 redirect 해줍니다. 이제 이 인가코드를 서버로 보내 토큰을 발급받으면 됩니다. 

네이버에서 인가코드를 발급해주어 redirect시켜준 주소

redirect 된 주소의 경로를 살펴보면 /auth/naver/callback 뒤의 query string으로 code가 딸려온 것을 볼 수 있습니다. 이 값을 서버로 전송하면 되는 것입니다.

// pages/auth/kakao/callback.tsx
import Layout from '@components/common/Layout';
import { useEffect } from 'react';
import instance from '@apis/_axios/instance';
import { setToken } from '@utils/localStorage/token';
import { Token } from '@utils/localStorage/token';
import { useRouter } from 'next/router';

export default function KakaoCallback() {
  const router = useRouter();

  useEffect(() => {
    const code = new URL(window.location.href).searchParams.get('code');
    instance
      .get(`/oauth2/kakao?code=${code}`)
      .then((res) => {
        console.log(res.data);
        const token: Token = {
          access: res.data.accessToken,
          refresh: res.data.refreshToken,
        };
        if (token) setToken(token);
      })
      .then(() => router.push('/home'))
      .catch((err) => {
        console.log(err);
      });
  }, []);

  return (
    <Layout>
      <div className="flex justify-center items-center h-screen">
        <div className="grid gap-2">
          <div className="flex items-center justify-center space-x-2">
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce1"></div>
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce2"></div>
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce3"></div>
          </div>
        </div>
      </div>
    </Layout>
  );
}
// pages/auth/naver/callback.tsx
import Layout from '@components/common/Layout';
import { useEffect } from 'react';
import instance from '@apis/_axios/instance';
import { setToken } from '@utils/localStorage/token';
import { Token } from '@utils/localStorage/token';
import { useRouter } from 'next/router';

export default function NaverCallback() {
  const router = useRouter();

  useEffect(() => {
    const code = new URL(window.location.href).searchParams.get('code');
    const state = new URL(window.location.href).searchParams.get('state');
    instance
      .get(`/oauth2/naver?code=${code}&state=${state}`)
      .then((res) => {
        const token: Token = {
          access: res.data.accessToken,
          refresh: res.data.refreshToken,
        };
        if (token) setToken(token);
      })
      .then(() => router.push('/home'))
      .catch((err) => {
        console.log(err);
      });
  }, []);

  return (
    <Layout>
      <div className="flex justify-center items-center h-screen">
        <div className="grid gap-2">
          <div className="flex items-center justify-center space-x-2">
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce1"></div>
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce2"></div>
            <div className="w-3 h-3 bg-[#F5535D] rounded-full animate-bounce3"></div>
          </div>
        </div>
      </div>
    </Layout>
  );
}

서버는 클라이언트로부터 받은 값을 가지고 카카오 혹은 네이버로 보내 값이 일치하는 경우 토큰을 발급하게 되고 이는 다시 프런트로 전달되어 로그인에 사용합니다. 저희는 서버로부터 전달받은 accessToken과 refreshToken은 localStorage에 저장하였습니다.

 

아래는 프로젝트에서 실제로 구현한 코드입니다.

/pages/auth/kakao/callback.tsx

/pages/auth/naver/callback.tsx


🤔 3. 느낀 점 / 배운 점 / 추가로 공부할 것

1. AccessToken과 RefreshToken을 모두 localStorage에 저장하기에 아무래도 보안에 취약할 것이라 생각하였다.

localStorage에 저장하면 왜 보안에 취약하고, 다른 어떤 방식이 있을까?

이 방식은 XSS공격에 취약하다. 이는 악의적인 JS코드를 웹 브라우저에서 실행하는 공격이다. 이 방법으로 피해자 브라우저에 저장된 중요 정보들을 탈취 가능하다.

localStorage 외에 다른 방식으로는 쿠키가 있다. 쿠키 역시 JS로 접근이 가능하므로 HTTP only 옵션을 걸어주어야 한다. HTTPS가 적용되지 않는 이미지 등으로 인해 쿠키를 탈취당할 수 있으므로 secure 옵션도 걸어주어야 한다.

그렇다면 쿠키에 담아도 될까? 쿠키에 토큰을 담으면 CSRF공격에 취약하다. 그렇지만, Refresh Token으로 Access Token을 재발급 받는 요청 외에 인증 인가가 필요한 작업들에 Refresh Token으로 접근할 수 없기에 Refersh Token은 쿠키에 저장해도 된다. 최고의 방법은 Refresh Token을 httpOnly쿠키로 설정하고 url이 새로고침 될 때마다 Refresh Token을 request에 담아 새로운 Access Token을 발급 받는다. 이 발급 받은 Access Token은 JS private variable에 저장하는 방식이다. 이를 통해 쿠키를 이용하여 XSS를 막고, Refresh Token방식을 이용하여 CSRF를 막을 수 있다.

# XSS(Crose Site Scripting)공격 : 권한이 없는 사용자가 악의적인 용도로 웹 사이트에 스크립트를 삽입하는 공격 기법

# CSRF 공격(Cross Site Request Forgery) : 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격

참고 1, 참고 2

다음 번에는 백엔드와 협의하여 Refresh Token은 cookie에, Access Token은 JS private variable에 저장하는 방식으로 구현하여 보안을 더 보완해야겠다. 

 

2. XSS, CSRF 등 보안 관련해서 더 깊이있게 공부를 해봐야겠다. 백엔드 뿐 아니라, 프론트엔드도 이에 대한 역량이 필수적이라 생각한다.

저작자표시 (새창열림)

'개발 일지 > 개발 일지' 카테고리의 다른 글

[개발 일지] React성능 최적화 (2) : React Query의 캐싱 기능  (1) 2023.05.05
[개발 일지] React성능 최적화 (1) : React.memo로 사용자 경험 개선하기  (0) 2023.05.03
[개발 일지] Next.js에서 접근성 향상하기 (접근성 점수 84점 -> 97점 향상시킨 썰)  (0) 2023.04.27
[개발 일지] React Query - Custom Hook 만들기 (+Typescript)  (0) 2023.04.13
[개발 일지] Intersection Observer API & react-query를 이용한 무한스크롤 구현  (0) 2023.02.28
[개발 일지] Suspense를 이용하여 모든 api에 Loading 화면을 설정하자 (NextJS, Suspense)  (0) 2023.02.18
[개발 일지] Axios instance 설정 끝장내기 (Axios, instance, interceptors, JWT)  (0) 2023.02.11
[개발 일지] 채팅 기능을 구현해보자 (Websocket, Stomp, SockJS)  (0) 2023.02.10
  1. 📚 1. 사전 지식
  2. JWT란?
  3. 소셜 로그인이란? oAuth?
  4. 소셜 로그인의 전체적인 흐름
  5. 🤩 2. 실제로 구현하기
  6. 1. 카카오/네이버로부터 인가코드 발급 받기
  7. 2. 발급받은 인가코드로 서버에 요청해서 토큰 발급받기
  8. 🤔 3. 느낀 점 / 배운 점 / 추가로 공부할 것
'개발 일지/개발 일지' 카테고리의 다른 글
  • [개발 일지] Next.js에서 접근성 향상하기 (접근성 점수 84점 -> 97점 향상시킨 썰)
  • [개발 일지] React Query - Custom Hook 만들기 (+Typescript)
  • [개발 일지] Intersection Observer API & react-query를 이용한 무한스크롤 구현
  • [개발 일지] Suspense를 이용하여 모든 api에 Loading 화면을 설정하자 (NextJS, Suspense)
피터s
피터s
1년차 프론트엔드 개발자입니다 😣 아직 열심히 배우는 중이에요! 리액트를 하고있어요 :) - gueit214@naver.com - https://github.com/gueit214
피터s
피터의 성장기록
피터s
전체
오늘
어제
  • 분류 전체보기 (200)
    • 코딩 테스트 (25)
      • 프로그래머스 (16)
      • LeetCode (8)
      • 백준 (1)
    • 개발 독서 일지 (1)
    • 기업 분석 (4)
    • 개발 일지 (19)
      • 최신기술 도전기 (1)
      • 에러 처리 (5)
      • 개발 일지 (12)
    • 개발 일상 (36)
      • 개발 회고 (22)
      • 개발 이야기 (12)
      • 개발 서적 (1)
    • 취업 관련 지식 (11)
    • 알고리즘 (17)
    • WebProgramming (84)
      • WebProgramming (8)
      • HTML (5)
      • CSS (8)
      • JS (21)
      • React (40)

블로그 메뉴

  • About
  • 2022년 개발 성장기
  • 앞으로의 계획
  • github
  • 일상 blog

공지사항

인기 글

태그

  • 해커톤
  • 누적합
  • lv3
  • Retry
  • 1일 1커밋 후기
  • dfs
  • 스터디 후기
  • BFS
  • 구름톤
  • 개발 회고
  • 구름
  • KAKAO BLIND
  • LV2
  • 카카오
  • Kakao Tech Internship
  • 1년 회고
  • 개발 is life
  • 함수
  • 카카오 채용연계형 인턴십
  • Union-find
  • 개발 일상
  • 반복문

최근 댓글

최근 글

hELLO · Designed By 정상우.
피터s
[개발 일지] JWT + 소셜 로그인을 정복해보자 (React + Spring Boot, Kakao Login, Naver Login)
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.