다음 토이프로젝트의 개발 기록이다.
OAuth를 활용한 소셜 로그인을 React 프로젝트에 적용하는 방법을 알아보자.
네이버 로그인 지원 환경은 아래와 같이 크게 4개의 환경으로 구분할 수 있다
다음 링크에서 애플리케이션을 등록하고 서비스 URL과 인증 요청 시 콜백으로 인증 정보를 담아 줄 URL을 설정해준다.
네이버 로그인 연동을 진행하기 위해서는 네이버 로그인 버튼을 클릭하였을 때 이동할 '네이버 로그인' URL을 먼저 생성하여야 한다.
이 과정에서 사용자는 네이버에 로그인인증을 수행하고 네이버 로그인 연동 동의과정을 수행할 수 있있다.
사용자가 로그인 연동에 동의하였을 경우 동의 정보를 포함하여 Callback URL로 전송된다.
요청 URL 정보
메서드 | 요청 URL | 출력 포맷 | 설명 |
---|---|---|---|
GET / POST | https://nid.naver.com/oauth2.0/authorize | URL 리다이렉트 | 네이버 로그인 인증 요청 |
요청 변수 정보
요청 변수명 | 타입 | 필수 여부 | 기본값 | 설명 |
---|---|---|---|---|
response_type | string | Y | code | 인증 과정에 대한 내부 구분값으로 'code'로 전송해야 함 |
client_id | string | Y | - | 애플리케이션 등록 시 발급받은 Client ID 값 |
redirect_uri | string | Y | - | 애플리케이션을 등록 시 입력한 Callback URL 값으로 URL 인코딩을 적용한 값 |
state | string | Y | - | 사이트 간 요청 위조(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 응답 정보
필드 | 타입 | 설명 |
---|---|---|
code | string | 네이버 로그인 인증에 성공하면 반환받는 인증 코드, 접근 토큰(access token) 발급에 사용 |
state | string | 사이트 간 요청 위조 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰으로 URL 인코딩을 적용한 값 |
error | string | 네이버 로그인 인증에 실패하면 반환받는 에러 코드 |
error_description | string | 네이버 로그인 인증에 실패하면 반환받는 에러 메시지 |
로그인에 성공하면 해당 URL로 리다이렉팅하면서 쿼리에 code가 함께 온다. 해당 코드를 사용해서 accessToken과 refreshToken을 발급받을 수 있게 된다.
Callback으로 전달받은 정보를 이용하여 접근 토큰을 발급받을 수 있다. 접근 토큰은 사용자가 인증을 완료했다는 것을 보장할 수 있는 인증 정보다.
이 접근 토큰을 이용하여 프로필 API를 호출하거나 오픈API를 호출하는것이 가능하다.
Callback으로 전달받은 'code' 값을 이용하여 '접근토큰발급API'를 호출하게 되면 API 응답으로 접근토큰에 대한 정보를 받을 수 있다.
'code' 값을 이용한 API호출은 최초 1번만 수행할 수 있으며 접근 토큰 발급이 완료되면 사용된 'code'는 더 이상 재사용할수 없다.
요청 URL 정보
메서드 | 요청 URL | 출력 포맷 | 설명 |
---|---|---|---|
GET / POST | https://nid.naver.com/oauth2.0/token | json | 접근토큰 발급 요청 |
요청 변수 정보
요청 변수명 | 타입 | 필수 여부 | 기본값 | 설명 |
---|---|---|---|---|
grant_type | string | Y | - | 인증 과정에 대한 구분값1) 발급:'authorization_code'2) 갱신:'refresh_token'3) 삭제: 'delete' |
client_id | string | Y | - | 애플리케이션 등록 시 발급받은 Client ID 값 |
client_secret | string | Y | - | 애플리케이션 등록 시 발급받은 Client secret 값 |
code | string | 발급 때 필수 | - | 로그인 인증 요청 API 호출에 성공하고 리턴받은 인증코드값 (authorization code) |
state | string | 발급 때 필수 | - | 사이트 간 요청 위조(cross-site request forgery) 공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰값으로 URL 인코딩을 적용한 값을 사용 |
refresh_token | string | 갱신 때 필수 | - | 네이버 사용자 인증에 성공하고 발급받은 갱신 토큰(refresh token) |
access_token | string | 삭제 때 필수 | - | 기 발급받은 접근 토큰으로 URL 인코딩을 적용한 값을 사용 |
service_provider | string | 삭제 때 필수 | '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_token | string | 접근 토큰, 발급 후 expires_in 파라미터에 설정된 시간(초)이 지나면 만료됨 |
refresh_token | string | 갱신 토큰, 접근 토큰이 만료될 경우 접근 토큰을 다시 발급받을 때 사용 |
token_type | string | 접근 토큰의 타입으로 Bearer와 MAC의 두 가지를 지원 |
expires_in | integer | 접근 토큰의 유효 기간(초 단위) |
error | string | 에러 코드 |
error_description | string | 에러 메시지 |
적용한 코드는 다음과 같다.
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를 먼저 호출하여야 합니다.
요청 URL 정보
메서드 | 인증 | 요청 URL | 출력 포맷 | 설명 |
---|---|---|---|---|
GET / POST | OAuth2.0 | https://openapi.naver.com/v1/nid/me | JSON | 프로필 정보 조회 |
요청 변수 정보
요청 변수는 별도로 없으며, 요청 URL로 호출할 때 아래와 같이 요청 헤더에 접근 토큰 값을 전달하면 됩니다.
요청 헤더
요청 헤더명 | 설명 |
---|---|
Authorization | 접근 토큰(access token)을 전달하는 헤더다음과 같은 형식으로 헤더 값에 접근 토큰(access token)을 포함합니다. 토큰 타입은 "Bearer"로 값이 고정되어 있습니다.Authorization: {토큰 타입] {접근 토큰] |
요청문 예시
curl -XGET "https://openapi.naver.com/v1/nid/me" \
-H "Authorization: Bearer AAAAPIuf0L+qfDkMABQ3IJ8heq2mlw71DojBj3oc2Z6OxMQESVSrtR0dbvsiQbPbP1/cxva23n7mQShtfK4pchdk/rc="
출력 결과
필드 | 타입 | 필수 여부 | 설명 |
---|---|---|---|
resultcode | String | Y | API 호출 결과 코드 |
message | String | Y | 호출 결과 메시지 |
response/id | String | Y | 동일인 식별 정보동일인 식별 정보는 네이버 아이디마다 고유하게 발급되는 값입니다. |
response/nickname | String | Y | 사용자 별명 |
response/name | String | Y | 사용자 이름 |
response/email | String | Y | 사용자 메일 주소 |
response/gender | String | Y | 성별- F: 여성- M: 남성- U: 확인불가 |
response/age | String | Y | 사용자 연령대 |
response/birthday | String | Y | 사용자 생일(MM-DD 형식) |
response/profile_image | String | Y | 사용자 프로필 사진 URL |
response/birthyear | String | Y | 출생연도 |
response/mobile | String | Y | 휴대전화번호 |
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/, ''),
},
},
},
});
다음 링크에서 애플리케이션을 등록하고 서비스 URL과 인증 요청 시 콜백으로 인증 정보를 담아 줄 URL을 설정해준다.
https://developers.kakao.com/console/app
메서드 | URL | 인증 방식 |
---|---|---|
GET | https://kauth.kakao.com/oauth/authorize | - |
이름 | 타입 | 설명 | 필수 |
---|---|---|---|
client_id | String | 앱 REST API 키[내 애플리케이션] > [앱 키]에서 확인 가능 | O |
redirect_uri | String | 인가 코드를 전달받을 서비스 서버의 URI[내 애플리케이션] > [카카오 로그인] > [Redirect URI]에서 등록 | O |
response_type | String | code 로 고정 | O |
scope | String | 추가 항목 동의 받기 요청 시 사용사용자에게 동의 요청할 동의항목 ID 목록동의항목의 ID는 사용자 정보 또는 [내 애플리케이션] > [카카오 로그인] > [동의항목]에서 확인 가능쉼표(,)로 구분해 여러 개 전달 가능주의: OpenID Connect를 사용하는 앱의 경우, scope 파라미터 값에 openid 를 반드시 포함해야 함, 미포함 시 ID 토큰이 재발급되지 않음 (참고: 인가 코드 받기 API의 scope 파라미터) | X |
prompt | String | 동의 화면 요청 시 추가 상호작용을 요청할 때 사용쉼표(,)로 구분된 문자열 값 목록으로 전달다음 값 사용 가능login : 기존 사용자 인증 여부와 상관없이 사용자에게 카카오계정 로그인 화면을 출력하여 다시 사용자 인증을 수행하고자 할 때 사용, 카카오톡 인앱 브라우저에서는 이 기능이 제공되지 않음none : 사용자에게 동의 화면과 같은 대화형 UI를 노출하지 않고 인가 코드 발급을 요청할 때 사용, 인가 코드 발급을 위해 사용자의 동작이 필요한 경우 에러 응답 전달create : 사용자가 카카오계정 신규 가입 후 로그인하도록 할 때 사용, 카카오계정 가입 페이지로 이동 후, 카카오계정 가입 완료 후 동의 화면 출력select_account : 카카오계정 간편로그인을 요청할 때 사용, 브라우저에 카카오계정 로그인 세션이 있을 경우 자동 로그인 또는 계정 선택 화면 출력참고: 추가 기능 | X |
login_hint | String | 로그인 힌트 주기 요청 시 사용카카오계정 로그인 페이지의 ID란에 자동 입력할 값비고: 로그인하지 않은 사용자에게 카카오계정 로그인 페이지를 표시하는 상황에서만 동작참고: 카카오계정 로그인 시 이메일, 전화번호, 카카오메일 ID를 ID에 입력하여 로그인 가능 | X |
service_terms | String | 서비스 약관 선택해 동의 받기 요청 시 사용동의받을 서비스 약관 태그 목록서비스 약관 태그는 [내 애플리케이션] > [카카오 로그인] > [간편가입]에서 확인 가능쉼표(,)로 구분된 문자열 값 목록으로 전달 | X |
state | String | 카카오 로그인 과정 중 동일한 값을 유지하는 임의의 문자열(정해진 형식 없음)Cross-Site Request Forgery(CSRF) 공격으로부터 카카오 로그인 요청을 보호하기 위해 사용각 사용자의 로그인 요청에 대한 state 값은 고유해야 함인가 코드 요청, 인가 코드 응답, 토큰 발급 요청의 state 값 일치 여부로 요청 및 응답 유효성 확인 가능 | X |
nonce | String | OpenID Connect를 통해 ID 토큰을 함께 발급받을 경우, ID 토큰 재생 공격을 방지하기 위해 사용ID 토큰 유효성 검증 시 대조할 임의의 문자열(정해진 형식 없음) | X |
인가 코드 받기 요청의 응답은 HTTP 302 리다이렉트되어, redirect_uri
에 GET
요청으로 전달됩니다. 해당 요청은 아래와 같은 쿼리 파라미터를 포함합니다.
이름 | 타입 | 설명 | 필수 |
---|---|---|---|
code | String | 토큰 받기 요청에 필요한 인가 코드 | X |
error | String | 인증 실패 시 반환되는 에러 코드 | X |
error_description | String | 인증 실패 시 반환되는 에러 메시지 | X |
state | String | 요청 시 전달한 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_uri
로 HTTP 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 | 인증 방식 |
---|---|---|
POST | https://kauth.kakao.com/oauth/token | - |
인가 코드로 토큰 발급을 요청한다. 인가 코드 받기만으로는 카카오 로그인이 완료되지 않으며, 토큰 받기까지 마쳐야 카카오 로그인을 정상적으로 완료할 수 있다.
필수 파라미터를 포함해 POST
로 요청한다. 요청 성공 시 응답은 토큰과 토큰 정보를 포함한다. OpenID Connect를 사용하는 앱인 경우, 응답에 ID 토큰이 함께 포함된다. 각 토큰의 역할과 만료 시간에 대한 자세한 정보는 토큰 정보에서 확인할 수 있다.
액세스 토큰으로 사용자 정보 가져오기와 같은 카카오 API를 호출할 수 있다. 토큰 정보 보기로 액세스 토큰 유효성 검증 후, 사용자 정보 가져오기를 요청해 필요한 사용자 정보를 받아 서비스 회원 가입 및 로그인을 완료한다.
이름 | 설명 | 필수 |
---|---|---|
Content-type | Content-type: application/x-www-form-urlencoded;charset=utf-8 요청 데이터 타입 | O |
이름 | 타입 | 설명 | 필수 |
---|---|---|---|
grant_type | String | authorization_code 로 고정 | O |
client_id | String | 앱 REST API 키[내 애플리케이션] > [앱 키]에서 확인 가능 | O |
redirect_uri | String | 인가 코드가 리다이렉트된 URI | O |
code | String | 인가 코드 받기 요청으로 얻은 인가 코드 | O |
client_secret | String | 토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드[내 애플리케이션] > [카카오 로그인] > [보안]에서 설정 가능ON 상태인 경우 필수 설정해야 함 | X |
이름 | 타입 | 설명 | 필수 |
---|---|---|---|
token_type | String | 토큰 타입, bearer 로 고정 | O |
access_token | String | 사용자 액세스 토큰 값 | O |
id_token | String | ID 토큰 값OpenID Connect 확장 기능을 통해 발급되는 ID 토큰, Base64 인코딩 된 사용자 인증 정보 포함제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우또는 scope 에 openid 를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우 | X |
expires_in | Integer | 액세스 토큰과 ID 토큰의 만료 시간(초)참고: 액세스 토큰과 ID 토큰의 만료 시간은 동일 | O |
refresh_token | String | 사용자 리프레시 토큰 값 | O |
refresh_token_expires_in | Integer | 리프레시 토큰 만료 시간(초) | O |
scope | String | 인증된 사용자의 정보 조회 권한 범위범위가 여러 개일 경우, 공백으로 구분참고: 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/, ''),
},
},
},
});
메서드 | URL | 인증 방식 |
---|---|---|
GET/POST | https://kapi.kakao.com/v2/user/me | 액세스 토큰서비스 앱 어드민 키 |
권한 | 사전 설정 | 카카오 로그인 | 동의항목 |
---|---|---|---|
- | 플랫폼 등록카카오 로그인 활성화동의항목 | 필요 | 필요:사용자 정보를 요청할 모든 동의항목 |
현재 로그인한 사용자의 정보를 불러온다. 이 API를 사용하려면 동의항목 설정을 참고하여 각 응답 필드에 필요한 동의항목을 설정해야 한다. 동의항목이 설정되어 있더라도 사용자가 동의하지 않으면 사용자 정보를 받을 수 없다. 동의 내역 확인하기 API를 통해 사용자가 동의한 동의항목을 먼저 확인할 수 있다.
사용자 액세스 토큰 또는 어드민 키를 헤더(Header)에 담아 GET
또는 POST
로 요청한다. 사용자 정보 요청 REST API는 사용자 액세스 토큰을 사용하는 방법, 앱 어드민 키를 사용하는 방법 두 가지로 제공되며, 어드민 키는 보안에 유의하여 사용해야 하므로 서버에서 호출할 때만 사용한다.
이름 | 설명 | 필수 |
---|---|---|
Authorization | Authorization: Bearer ${ACCESS_TOKEN} 인증 방식, 액세스 토큰으로 인증 요청 | O |
Content-type | Content-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/, ''),
},
},
},
});