OAuth를 활용한 네이버, 카카오 로그인 구현

sham·2024년 10월 28일
0

SkyScope 개발일지

목록 보기
9/12

다음 토이프로젝트의 개발 기록이다.

개요

OAuth를 활용한 소셜 로그인을 React 프로젝트에 적용하는 방법을 알아보자.

네이버 로그인

네이버 로그인 개발가이드 - LOGIN

지원 환경 확인

네이버 로그인 지원 환경은 아래와 같이 크게 4개의 환경으로 구분할 수 있다

  • PC-WEB / MOBILE-WEB : 웹서비스 환경
  • Android : 안드로이드 애플리케이션 환경
  • iOS : iOS애플리케이션 환경
  • Windows : 윈도우즈 데스크탑 프로그램 환경

애플리케이션 등록 및 세팅

다음 링크에서 애플리케이션을 등록하고 서비스 URL과 인증 요청 시 콜백으로 인증 정보를 담아 줄 URL을 설정해준다.

사전 준비 사항 - Open API 가이드

네이버 로그인 인가 코드 받기

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 연동을 진행하기 위해서는 네이버 로그인 버튼을 클릭하였을 때 이동할 '네이버 로그인' URL을 먼저 생성하여야 한다.

이 과정에서 사용자는 네이버에 로그인인증을 수행하고 네이버 로그인 연동 동의과정을 수행할 수 있있다.

사용자가 로그인 연동에 동의하였을 경우 동의 정보를 포함하여 Callback URL로 전송된다.

요청 URL 정보

메서드요청 URL출력 포맷설명
GET / POSThttps://nid.naver.com/oauth2.0/authorizeURL 리다이렉트네이버 로그인 인증 요청

요청 변수 정보

요청 변수명타입필수 여부기본값설명
response_typestringYcode인증 과정에 대한 내부 구분값으로 'code'로 전송해야 함
client_idstringY-애플리케이션 등록 시 발급받은 Client ID 값
redirect_uristringY-애플리케이션을 등록 시 입력한 Callback URL 값으로 URL 인코딩을 적용한 값
statestringY-사이트 간 요청 위조(cross-site request forgery) 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰값으로 URL 인코딩을 적용한 값을 사용

요청문 샘플

https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=CLIENT_ID&state=STATE_STRING&redirect_uri=CALLBACK_URL

실제 적용 코드

적용한 코드는 다음과 같다.

클릭 시 해당 url로 이동하게 된다. 로그인을 진행하면 리다이렉팅으로 설정한 URL로 코드가 담겨 리다이렉팅된다.

import { styled } from 'styled-components';
import { useNavigate } from 'react-router-dom';

const api_url = import.meta.env.VITE_NAVER_URL;
const client_id = import.meta.env.VITE_NAVER_ID;
const redirect_uri = import.meta.env.VITE_NAVER_REDIRECT;

const LoginPage = () => {
  const onClickNaver = async () => {
    const url =
      api_url + `/authorize?response_type=code&client_id=${client_id}&redirect_uri=${redirect_uri}&state=1234`;
    console.log(url);
    window.location.href = url;
  };

  return (
    <LoginPageContainer>
      <h1>Login Page</h1>

      <img onClick={onClickNaver} src='/login_naver/btnW_완성형.png' alt='naver_login' />
    </LoginPageContainer>
  );
};

export default LoginPage;

const LoginPageContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f7f7f7;
  img {
    cursor: pointer;
    width: 200px;
  
}  
    }
`;

네이버 로그인 응답 예제

네이버 로그인 인증 요청 API를 호출했을 때 사용자가 네이버로 로그인하지 않은 상태이면 네이버 로그인 화면으로 이동하고, 사용자가 네이버에 로그인한 상태이면 기본 정보 제공 동의 확인 화면으로 이동한다.

네이버 로그인과 정보 제공 동의 과정이 완료되면 콜백 URL에 code값과 state 값이 URL 문자열로 전송됩니다. code 값은 접근 토큰 발급 요청에 사용된다.

API 요청 실패시에는 에러 코드와 에러 메시지가 전송된다.

Callback 응답 정보

필드타입설명
codestring네이버 로그인 인증에 성공하면 반환받는 인증 코드, 접근 토큰(access token) 발급에 사용
statestring사이트 간 요청 위조 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰으로 URL 인코딩을 적용한 값
errorstring네이버 로그인 인증에 실패하면 반환받는 에러 코드
error_descriptionstring네이버 로그인 인증에 실패하면 반환받는 에러 메시지

로그인에 성공하면 해당 URL로 리다이렉팅하면서 쿼리에 code가 함께 온다. 해당 코드를 사용해서 accessToken과 refreshToken을 발급받을 수 있게 된다.

접근 토큰 발급 요청

Callback으로 전달받은 정보를 이용하여 접근 토큰을 발급받을 수 있다. 접근 토큰은 사용자가 인증을 완료했다는 것을 보장할 수 있는 인증 정보다.

이 접근 토큰을 이용하여 프로필 API를 호출하거나 오픈API를 호출하는것이 가능하다.

Callback으로 전달받은 'code' 값을 이용하여 '접근토큰발급API'를 호출하게 되면 API 응답으로 접근토큰에 대한 정보를 받을 수 있다.

'code' 값을 이용한 API호출은 최초 1번만 수행할 수 있으며 접근 토큰 발급이 완료되면 사용된 'code'는 더 이상 재사용할수 없다.

요청 URL 정보

메서드요청 URL출력 포맷설명
GET / POSThttps://nid.naver.com/oauth2.0/tokenjson접근토큰 발급 요청

요청 변수 정보

요청 변수명타입필수 여부기본값설명
grant_typestringY-인증 과정에 대한 구분값1) 발급:'authorization_code'2) 갱신:'refresh_token'3) 삭제: 'delete'
client_idstringY-애플리케이션 등록 시 발급받은 Client ID 값
client_secretstringY-애플리케이션 등록 시 발급받은 Client secret 값
codestring발급 때 필수-로그인 인증 요청 API 호출에 성공하고 리턴받은 인증코드값 (authorization code)
statestring발급 때 필수-사이트 간 요청 위조(cross-site request forgery) 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰값으로 URL 인코딩을 적용한 값을 사용
refresh_tokenstring갱신 때 필수-네이버 사용자 인증에 성공하고 발급받은 갱신 토큰(refresh token)
access_tokenstring삭제 때 필수-기 발급받은 접근 토큰으로 URL 인코딩을 적용한 값을 사용
service_providerstring삭제 때 필수'NAVER'인증 제공자 이름으로 'NAVER'로 세팅해 전송

요청문 샘플

https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=jyvqXeaVOVmV&client_secret=527300A0_COq1_XV33cf&code=EIc5bFrl4RibFls1&state=9kgsGTfH4j7IyAkg

응답 정보

필드타입설명
access_tokenstring접근 토큰, 발급 후 expires_in 파라미터에 설정된 시간(초)이 지나면 만료됨
refresh_tokenstring갱신 토큰, 접근 토큰이 만료될 경우 접근 토큰을 다시 발급받을 때 사용
token_typestring접근 토큰의 타입으로 Bearer와 MAC의 두 가지를 지원
expires_ininteger접근 토큰의 유효 기간(초 단위)
errorstring에러 코드
error_descriptionstring에러 메시지

실제 적용 코드

적용한 코드는 다음과 같다.

import axios, { AxiosInstance } from 'axios';

const url = import.meta.env.VITE_NAVER_TOKEN_URL;
const naver_id = import.meta.env.VITE_NAVER_ID;
const naver_sercret = import.meta.env.VITE_NAVER_SECRET;

const instance: AxiosInstance = axios.create({
  baseURL: url,
  timeout: 3000,
  headers: {
    'Content-Type': 'application/json',
  },
});
const getNaverToken = async (type: string, requirement: string) => {
  try {
    const url = `/token?grant_type=${type}&client_id=${naver_id}&client_secret=${naver_sercret}&${requirement}`;
    const response = await instance.get(url);
    const data = response.data;
    return data;
  } catch (e) {
    let message;
    if (e instanceof Error) message = e.message;
    else message = '/getNaverToken error';
    console.error(message);
  }
};

export default getNaverToken;
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { getNaverToken } from '@src/API';

const OAuthPage = () => {
  const location = useLocation();
  const navigate = useNavigate();

  const code = new URLSearchParams(location.search).get('code');
  // naver인지, kakao인지 구분해야함
  const pathSegments = location.pathname.split('/'); // 경로를 '/'로 나눠 배열로 저장
  console.log('code : ', code);
  console.log('pathSegments : ', pathSegments);

  const getToken = async () => {
    const type = pathSegments.pop();
    switch (type) {
      case 'naver': {
        const result = await getNaverToken('authorization_code', `code=${code}&state=1234`);
        if (!result) return;
        const { access_token, refresh_token } = result;
        localStorage.setItem('accessToken', access_token);
        localStorage.setItem('refreshToken', refresh_token);
        localStorage.setItem('oauthType', 'naver');
        // navigate('/');
        break;
      }
      default:
        console.log('error!');
    }
  };

  useEffect(() => {
    getToken();
  }, []);
  return (
    <div>
      <h1>OAuth Page</h1>
    </div>
  );
};

export default OAuthPage;

프록시 세팅하기

일반적인 방식으로 로컬에서 token API를 요청하면 CORS 에러가 발생한다.

proxy 설정을 통해 url을 target으로 바꿔치기해 로컬 환경에서의 CORS 에러를 해결할 수 있다.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Define aliases for directories
      '@src': path.resolve(__dirname, 'src'),
      // Add more aliases as needed
    },
  },
  server: {
      '/api/naver/token': {
        target: 'https://nid.naver.com/oauth2.0',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/naver\/token/, ''),
      },
    },
  },
});

접근 토큰을 이용하여 프로필 API 호출하기

접근 토큰을 이용하면 프로필 정보 조회 API를 호출하거나 오픈 API를 호출하는것이 가능합니다.

사용자 로그인 정보를 획득하기 위해서는 프로필 정보 조회 API를 먼저 호출하여야 합니다.

요청 URL 정보

메서드인증요청 URL출력 포맷설명
GET / POSTOAuth2.0https://openapi.naver.com/v1/nid/meJSON프로필 정보 조회

요청 변수 정보

요청 변수는 별도로 없으며, 요청 URL로 호출할 때 아래와 같이 요청 헤더에 접근 토큰 값을 전달하면 됩니다.

요청 헤더

요청 헤더명설명
Authorization접근 토큰(access token)을 전달하는 헤더다음과 같은 형식으로 헤더 값에 접근 토큰(access token)을 포함합니다. 토큰 타입은 "Bearer"로 값이 고정되어 있습니다.Authorization: {토큰 타입] {접근 토큰]

요청문 예시

curl  -XGET "https://openapi.naver.com/v1/nid/me" \
      -H "Authorization: Bearer AAAAPIuf0L+qfDkMABQ3IJ8heq2mlw71DojBj3oc2Z6OxMQESVSrtR0dbvsiQbPbP1/cxva23n7mQShtfK4pchdk/rc="

출력 결과

필드타입필수 여부설명
resultcodeStringYAPI 호출 결과 코드
messageStringY호출 결과 메시지
response/idStringY동일인 식별 정보동일인 식별 정보는 네이버 아이디마다 고유하게 발급되는 값입니다.
response/nicknameStringY사용자 별명
response/nameStringY사용자 이름
response/emailStringY사용자 메일 주소
response/genderStringY성별- F: 여성- M: 남성- U: 확인불가
response/ageStringY사용자 연령대
response/birthdayStringY사용자 생일(MM-DD 형식)
response/profile_imageStringY사용자 프로필 사진 URL
response/birthyearStringY출생연도
response/mobileStringY휴대전화번호

실제 적용 코드

axios 단에 인터셉터를 추가해 토큰이 없거나 만료되었을 경우의 처리를 해주었다.

import axios, { AxiosInstance } from 'axios';

const url = import.meta.env.VITE_NAVER_URL;
const client_id = import.meta.env.VITE_NAVER_ID;
const client_secret = import.meta.env.VITE_NAVER_SECRET;

const instance: AxiosInstance = axios.create({
  baseURL: url,
  timeout: 3000,
  headers: {
    'Content-Type': 'application/json',
  },
});

instance.interceptors.request.use(config => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers['Authorization'] = `Bearer ${accessToken}`;
  }
  return config;
});

const getNaverInfo = async () => {
  try {
    const response = await instance.get('/v1/nid/me');
    const data = response.data.response;
    return data;
  } catch (e) {
    console.log('error : ', e);
    let message;
    if (e instanceof Error) message = e.message;
    else message = '/getUltraSrtNcst error';
    console.error(message);
  }
};

export default getNaverInfo;

만료된 access_token의 경우 재발급을 신청하는 케이스.

프록시 세팅하기

token 요청 때와 똑같이, 프록시 설정으로 로컬 환경에서의 CORS 에러를 방지해줄 수 있다.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Define aliases for directories
      '@src': path.resolve(__dirname, 'src'),
      // Add more aliases as needed
    },
  },
  server: {
    proxy: {
      '/api/naver/info': {
        target: 'https://openapi.naver.com',
        changeOrigin: true,
        // /api/naver/info를 제거하고 요청을 전달
        rewrite: path => path.replace(/^\/api\/naver\/info/, ''),
      },
      '/api/naver/token': {
        target: 'https://nid.naver.com/oauth2.0',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/naver\/token/, ''),
      },
    },
  },
});

카카오 로그인

Kakao Developers

애플리케이션 등록 및 세팅

다음 링크에서 애플리케이션을 등록하고 서비스 URL과 인증 요청 시 콜백으로 인증 정보를 담아 줄 URL을 설정해준다.

https://developers.kakao.com/console/app

카카오 로그인 인가 코드 받기

기본 정보

메서드URL인증 방식
GEThttps://kauth.kakao.com/oauth/authorize-

요청

이름타입설명필수
client_idString앱 REST API 키[내 애플리케이션] > [앱 키]에서 확인 가능O
redirect_uriString인가 코드를 전달받을 서비스 서버의 URI[내 애플리케이션] > [카카오 로그인] > [Redirect URI]에서 등록O
response_typeStringcode로 고정O
scopeString추가 항목 동의 받기 요청 시 사용사용자에게 동의 요청할 동의항목 ID 목록동의항목의 ID는 사용자 정보 또는 [내 애플리케이션] > [카카오 로그인] > [동의항목]에서 확인 가능쉼표(,)로 구분해 여러 개 전달 가능주의: OpenID Connect를 사용하는 앱의 경우, scope 파라미터 값에 openid를 반드시 포함해야 함, 미포함 시 ID 토큰이 재발급되지 않음 (참고: 인가 코드 받기 API의 scope 파라미터)X
promptString동의 화면 요청 시 추가 상호작용을 요청할 때 사용쉼표(,)로 구분된 문자열 값 목록으로 전달다음 값 사용 가능login: 기존 사용자 인증 여부와 상관없이 사용자에게 카카오계정 로그인 화면을 출력하여 다시 사용자 인증을 수행하고자 할 때 사용, 카카오톡 인앱 브라우저에서는 이 기능이 제공되지 않음none: 사용자에게 동의 화면과 같은 대화형 UI를 노출하지 않고 인가 코드 발급을 요청할 때 사용, 인가 코드 발급을 위해 사용자의 동작이 필요한 경우 에러 응답 전달create: 사용자가 카카오계정 신규 가입 후 로그인하도록 할 때 사용, 카카오계정 가입 페이지로 이동 후, 카카오계정 가입 완료 후 동의 화면 출력select_account카카오계정 간편로그인을 요청할 때 사용, 브라우저에 카카오계정 로그인 세션이 있을 경우 자동 로그인 또는 계정 선택 화면 출력참고: 추가 기능X
login_hintString로그인 힌트 주기 요청 시 사용카카오계정 로그인 페이지의 ID란에 자동 입력할 값비고: 로그인하지 않은 사용자에게 카카오계정 로그인 페이지를 표시하는 상황에서만 동작참고: 카카오계정 로그인 시 이메일, 전화번호, 카카오메일 ID를 ID에 입력하여 로그인 가능X
service_termsString서비스 약관 선택해 동의 받기 요청 시 사용동의받을 서비스 약관 태그 목록서비스 약관 태그는 [내 애플리케이션] > [카카오 로그인] > [간편가입]에서 확인 가능쉼표(,)로 구분된 문자열 값 목록으로 전달X
stateString카카오 로그인 과정 중 동일한 값을 유지하는 임의의 문자열(정해진 형식 없음)Cross-Site Request Forgery(CSRF) 공격으로부터 카카오 로그인 요청을 보호하기 위해 사용각 사용자의 로그인 요청에 대한 state 값은 고유해야 함인가 코드 요청, 인가 코드 응답, 토큰 발급 요청의 state 값 일치 여부로 요청 및 응답 유효성 확인 가능X
nonceStringOpenID Connect를 통해 ID 토큰을 함께 발급받을 경우, ID 토큰 재생 공격을 방지하기 위해 사용ID 토큰 유효성 검증 시 대조할 임의의 문자열(정해진 형식 없음)X
  • auth_type: Deprecated, prompt를 사용하도록 변경

응답

인가 코드 받기 요청의 응답은 HTTP 302 리다이렉트되어, redirect_uriGET 요청으로 전달됩니다. 해당 요청은 아래와 같은 쿼리 파라미터를 포함합니다.

이름타입설명필수
codeString토큰 받기 요청에 필요한 인가 코드X
errorString인증 실패 시 반환되는 에러 코드X
error_descriptionString인증 실패 시 반환되는 에러 메시지X
stateString요청 시 전달한 state 값과 동일한 값X

요청 예제

https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}

응답 예제 : 사용자가 [동의하고 계속하기] 선택, 로그인 진행

HTTP/1.1 302 Found
Content-Length: 0
Location: ${REDIRECT_URI}?code=${AUTHORIZE_CODE}

응답 예제 : 로그인 취소

HTTP/1.1 302 Found
Content-Length: 0
Location: ${REDIRECT_URI}?error=access_denied&error_description=User%20denied%20access

카카오 로그인 동의 화면을 호출하고, 사용자 동의를 거쳐 인가 코드를 발급한다. 동의 화면은 앱에 설정된 동의항목에 대해 사용자에게 인가(동의)를 구한다. 인가 코드는 동의 화면을 통해 인가받은 동의항목 정보를 갖고 있으며, 인가 코드를 사용해 토큰 받기를 요청할 수 있다.

사용자는 동의 화면에서 서비스 이용에 필요한 동의항목에 동의하고 로그인하거나 로그인을 취소할 수 있다. 카카오 인증 서버는 사용자의 선택에 따라 요청 처리 결과를 담은 쿼리 스트링(Query string)을 redirect_uriHTTP 302 리다이렉트(Redirect)한다. Redirect URI는 [내 애플리케이션] > [카카오 로그인] > [Redirect URI]에 등록된 값 중 하나여야 한다.

실제 적용 코드

적용한 코드는 다음과 같다.

클릭 시 해당 url로 이동하게 된다. 로그인을 진행하면 리다이렉팅으로 설정한 URL로 코드가 담겨 리다이렉팅된다.

import { styled } from 'styled-components';

import { getNaverInfo } from '@src/API';
const naver_api_url = import.meta.env.VITE_NAVER_OAUTH_URL;
const naver_client_id = import.meta.env.VITE_NAVER_ID;
const naver_redirect_uri = import.meta.env.VITE_NAVER_REDIRECT;

const kakao_api_url = import.meta.env.VITE_KAKAO_OAUTH_URL;
const kakao_client_id = import.meta.env.VITE_KAKAO_REST_KEY;
const kakao_redirect_uri = import.meta.env.VITE_KAKAO_REDIRECT;

const LoginPage = () => {
  const onClickNaver = () => {
    const url =
      naver_api_url +
      `/authorize?response_type=code&client_id=${naver_client_id}&redirect_uri=${naver_redirect_uri}&state=1234`;
    console.log(url);
    window.location.href = url;
  };

  const onClickKakao = () => {
    const url =
      kakao_api_url + `/authorize?response_type=code&client_id=${kakao_client_id}&redirect_uri=${kakao_redirect_uri}`;
    console.log(url);
    window.location.href = url;
  };

  const onClickTestNaver = async () => {
    const result = await getNaverInfo();
    console.log('onClickTestNaver : ', result);
  };

  return (
    <LoginPageContainer>
      <h1>Login Page</h1>

      <img onClick={onClickNaver} src='/login_naver/btnW_완성형.png' alt='naver_login' />

      <button onClick={onClickTestNaver}>test naver Info</button>

      <div onClick={onClickKakao}>카카오 로그인</div>
    </LoginPageContainer>
  );
};

export default LoginPage;

const LoginPageContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f7f7f7;
  img {
    cursor: pointer;
    width: 200px;
  
}  
    }
`;

접근 토큰 발급 요청

토큰 받기

기본 정보

메서드URL인증 방식
POSThttps://kauth.kakao.com/oauth/token-

인가 코드로 토큰 발급을 요청한다. 인가 코드 받기만으로는 카카오 로그인이 완료되지 않으며, 토큰 받기까지 마쳐야 카카오 로그인을 정상적으로 완료할 수 있다.

필수 파라미터를 포함해 POST로 요청한다. 요청 성공 시 응답은 토큰과 토큰 정보를 포함한다. OpenID Connect를 사용하는 앱인 경우, 응답에 ID 토큰이 함께 포함된다. 각 토큰의 역할과 만료 시간에 대한 자세한 정보는 토큰 정보에서 확인할 수 있다.

액세스 토큰으로 사용자 정보 가져오기와 같은 카카오 API를 호출할 수 있다. 토큰 정보 보기로 액세스 토큰 유효성 검증 후, 사용자 정보 가져오기를 요청해 필요한 사용자 정보를 받아 서비스 회원 가입 및 로그인을 완료한다.

요청 헤더

이름설명필수
Content-typeContent-type: application/x-www-form-urlencoded;charset=utf-8요청 데이터 타입O

요청 본문

이름타입설명필수
grant_typeStringauthorization_code로 고정O
client_idString앱 REST API 키[내 애플리케이션] > [앱 키]에서 확인 가능O
redirect_uriString인가 코드가 리다이렉트된 URIO
codeString인가 코드 받기 요청으로 얻은 인가 코드O
client_secretString토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드[내 애플리케이션] > [카카오 로그인] > [보안]에서 설정 가능ON 상태인 경우 필수 설정해야 함X

응답 본문

이름타입설명필수
token_typeString토큰 타입, bearer로 고정O
access_tokenString사용자 액세스 토큰 값O
id_tokenStringID 토큰 값OpenID Connect 확장 기능을 통해 발급되는 ID 토큰, Base64 인코딩 된 사용자 인증 정보 포함제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우또는 scope에 openid를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우X
expires_inInteger액세스 토큰과 ID 토큰의 만료 시간(초)참고: 액세스 토큰과 ID 토큰의 만료 시간은 동일O
refresh_tokenString사용자 리프레시 토큰 값O
refresh_token_expires_inInteger리프레시 토큰 만료 시간(초)O
scopeString인증된 사용자의 정보 조회 권한 범위범위가 여러 개일 경우, 공백으로 구분참고: OpenID Connect가 활성화된 앱의 토큰 발급 요청인 경우, ID 토큰이 함께 발급되며 scope 값에 openid 포함X

실제 적용 코드

oauth로 리다이렉팅 되면 url의 code 부분을 가지고 token 요청을 날린다. 종류에 따라 oauth의 url을 다르게 지정하였으므로 switch 문에서 kakao 분기를 타고 kakao token을 요청한다.

import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { getNaverToken, getKakaoToken } from '@src/API';

const OAuthPage = () => {
  const location = useLocation();
  const navigate = useNavigate();

  const code = new URLSearchParams(location.search).get('code');
  // naver인지, kakao인지 구분해야함
  const pathSegments = location.pathname.split('/'); // 경로를 '/'로 나눠 배열로 저장
  console.log('code : ', code);
  console.log('pathSegments : ', pathSegments);

  const getToken = async () => {
    const type = pathSegments.pop();
    switch (type) {
      case 'naver': {
        const result = await getNaverToken('authorization_code', `code=${code}&state=1234`);
        if (!result) return;
        const { access_token, refresh_token } = result;
        localStorage.setItem('accessToken', access_token);
        localStorage.setItem('refreshToken', refresh_token);
        localStorage.setItem('oauthType', 'naver');
        // navigate('/');
        break;
      }
      case 'kakao': {
        const result = await getKakaoToken('authorization_code', `code=${code}`);
        if (!result) return;
        const { access_token, refresh_token } = result;
        localStorage.setItem('accessToken', access_token);
        localStorage.setItem('refreshToken', refresh_token);
        localStorage.setItem('oauthType', 'kakao');
        // navigate('/');
        break;
      }
      default:
        console.log('error!');
    }
  };

  useEffect(() => {
    getToken();
  }, []);
  return (
    <div>
      <h1>OAuth Page</h1>
    </div>
  );
};

export default OAuthPage;
import axios, { AxiosInstance } from 'axios';

const url = import.meta.env.VITE_KAKAO_OAUTH_URL;
const kakao_id = import.meta.env.VITE_KAKAO_REST_KEY;
const redirect_uri = import.meta.env.VITE_KAKAO_REDIRECT;
const instance: AxiosInstance = axios.create({
  baseURL: url,
  timeout: 3000,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    charset: 'utf-8',
  },
});

const getKakaoToken = async (type: string, requirement: string) => {
  try {
    const url = `/token?grant_type=${type}&client_id=${kakao_id}&redirect_uri=${redirect_uri}&${requirement}`;
    const response = await instance.get(url);
    const data = response.data;
    return data;
  } catch (e) {
    let message;
    if (e instanceof Error) message = e.message;
    else message = '/getNaverToken error';
    console.error(message);
  }
};

export default getKakaoToken;

프록시 세팅하기

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Define aliases for directories
      '@src': path.resolve(__dirname, 'src'),
      // Add more aliases as needed
    },
  },
  server: {
    proxy: {
      '/api/naver/oauth': {
        target: 'https://nid.naver.com/oauth2.0',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/naver\/oauth/, ''),
      },
      '/api/naver/info': {
        target: 'https://openapi.naver.com/v1',
        changeOrigin: true,
        // /api/naver/info를 제거하고 요청을 전달
        rewrite: path => path.replace(/^\/api\/naver\/info/, ''),
      },
      '/api/kakao/oauth': {
        target: 'https://kauth.kakao.com/oauth',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/kakao\/oauth/, ''),
      },
    },
  },
});

접근 토큰을 이용하여 프로필 API 호출하기

기본 정보

메서드URL인증 방식
GET/POSThttps://kapi.kakao.com/v2/user/me액세스 토큰서비스 앱 어드민 키
권한사전 설정카카오 로그인동의항목
-플랫폼 등록카카오 로그인 활성화동의항목필요필요:사용자 정보를 요청할 모든 동의항목

현재 로그인한 사용자의 정보를 불러온다. 이 API를 사용하려면 동의항목 설정을 참고하여 각 응답 필드에 필요한 동의항목을 설정해야 한다. 동의항목이 설정되어 있더라도 사용자가 동의하지 않으면 사용자 정보를 받을 수 없다. 동의 내역 확인하기 API를 통해 사용자가 동의한 동의항목을 먼저 확인할 수 있다.

사용자 액세스 토큰 또는 어드민 키를 헤더(Header)에 담아 GET 또는 POST로 요청한다. 사용자 정보 요청 REST API는 사용자 액세스 토큰을 사용하는 방법, 앱 어드민 키를 사용하는 방법 두 가지로 제공되며, 어드민 키는 보안에 유의하여 사용해야 하므로 서버에서 호출할 때만 사용한다.

요청 헤더

이름설명필수
AuthorizationAuthorization: Bearer ${ACCESS_TOKEN}인증 방식, 액세스 토큰으로 인증 요청O
Content-typeContent-type: application/x-www-form-urlencoded;charset=utf-8요청 데이터 타입O

요청 예제 : 액세스 토큰 방식으로 모든 정보 받기

curl -v -G GET "https://kapi.kakao.com/v2/user/me" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

응답 예제 : 성공, 앱에 닉네임 동의항목만 설정하고 사용자에게 동의받은 경우

HTTP/1.1 200 OK
{
    "id":123456789,
    "connected_at": "2022-04-11T01:45:28Z",
    "kakao_account": {
        "profile_nickname_needs_agreement": false,
        "profile": {
            "nickname": "홍길동"
        }
    },
    "properties":{
        "${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
        ...
    }
}

실제 적용 코드

네이버 info를 가져올 때와 유사한 방법을 사용했다.

import axios, { AxiosInstance } from 'axios';

import getKakaoToken from './getKakaoToken';

const url = import.meta.env.VITE_KAKAO_URL;

const instance: AxiosInstance = axios.create({
  baseURL: url,
  timeout: 3000,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    charset: 'utf-8',
  },
});

instance.interceptors.request.use(config => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers['Authorization'] = `Bearer ${accessToken}`;
  }
  return config;
});

const getKakaoInfo = async () => {
  try {
    const response = await instance.get('/user/me');
    console.log('response : ', response);
    const data = response.data;
    return data;
  } catch (e: any) {
    console.log('error : ', e);
    let message;
    if (e.response.status === 401) {
      const refreshToken = localStorage.getItem('refreshToken');
      const result = await getKakaoToken('refresh_token', `refresh_token=${refreshToken}`);
      if (!result) return null; // result가 없으면 null 반환;
      localStorage.setItem('accessToken', result.access_token);
      localStorage.setItem('refreshToken', result.refresh_token);
      const response = await instance.get('/user/me');
      const data = response.data.response;
      return data;
    }
    if (e instanceof Error) message = e.message;
    else message = '/getNaverInfo error';
    console.error(message);
  }
};

export default getKakaoInfo;

프록시 세팅하기

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Define aliases for directories
      '@src': path.resolve(__dirname, 'src'),
      // Add more aliases as needed
    },
  },
  server: {
    proxy: {
      '/api/naver/oauth': {
        target: 'https://nid.naver.com/oauth2.0',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/naver\/oauth/, ''),
      },
      '/api/naver/info': {
        target: 'https://openapi.naver.com/v1',
        changeOrigin: true,
        // /api/naver/info를 제거하고 요청을 전달
        rewrite: path => path.replace(/^\/api\/naver\/info/, ''),
      },
      '/api/kakao/oauth': {
        target: 'https://kauth.kakao.com/oauth',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/kakao\/oauth/, ''),
      },
      '/api/kakao/info': {
        target: 'https://kapi.kakao.com/v2',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api\/kakao\/info/, ''),
      },
    },
  },
});
profile
씨앗 개발자

0개의 댓글

관련 채용 정보