- 지역 기반 중고거래 사이트를 만드는 프로젝트에서 로그인 기능을 구현해야했고, 팀원들과 상의 결과 자체 로그인과 github OAuth 로그인 두 가지를 사용하기로 했다.
- 이 글에서는 OAuth에 대한 간단한 설명과 실제 프로젝트에서 github OAuth 로그인을 구현한 부분에 대해 설명하겠다.
OAuth(Open Authorization)란?
- OAuth는 권한 부여를 위한 공개된 인증 프로토콜(접근 권한을 위임하는 개방형 표준 프로토콜), 제3자 애플리케이션에 대한 보안 인증 및 권한 부여 프로토콜이다.
- 쉽게 설명해서 애플리케이션(Client)에서 별도의 회원가입 없이 사용자(Resource owner)를 대신하여 리소스 서버(Resource server)에서 제공하는 자원에 대한 접근 권한을 위임하는 방식으로, 인증(Authentication)과 인가(Authorization)를 받기 위한 것으로 볼 수 있다. 즉, 애플리케이션에서 사용자가 자신이 누구인지 확인하고, 원하는 정보를 얻도록 하는 과정을 말한다.
- 다음은 OAuth 주요 용어들에 대한 설명이다.
- 인증(Authentication) : 인증, 접근 자격이 있는지 검증하는 단계
- 인가(Authorization) : 인가, 자원에 접근할 권한을 부여하는 것으로 인가가 완료되면 리소스 접근 권한이 담긴 Access Token이 서비스 제공자(Client)에게 부여됨
- 사용자(Resource owner) : 리소스 소유자 또는 사용자. 보호된 자원에 접근할 수 있는 자격을 부여해 주는 주체
- 서비스 제공자(Client) : 보호된 자원을 사용하려고 접근 요청을 하는 애플리케이션
- Authorization Server : 인증을 담당하는 서버
- Resource Server : 리소스를 서비스 제공자(Client)에게 제공해주는 서버
OAuth 흐름
- 프로젝트에서 사용한 OAuth의 흐름은 다음과 같다.
- Resource owner는 로그인을 원하는 사용자, Client는 프로젝트의 애플리케이션, Authorization Server는 github 인증서버, Resource owner는 github 데이터 서버이다.
1 ~ 4. 로그인 요청 / 로그인 페이지 제공 / ID,PW 제공
- 사용자가 "Github 계정으로 로그인" 버튼을 누르면 github app에 등록된 client id, redirectUri(로그인이 성공하면 이동할 주소), socpe(필요한 정보의 범위)와 함께 github의 OAuth 인증 프로세스를 시작하기 위한 주소로 이동한다.
- redirectUri는 React 라우트로 AuthPage로 지정합니다. 즉, 사용자가 github 로그인을 완료하면 AuthPage로 이동한다.
const scope = 'user';
const redirectUri = `${URL}/redirect/oauth`;
const clientId = OAUTH_CLIENT_ID;
const githubLoginBtnHandler = () => {
window.location.href = `https://github.com/login/oauth/authorize?response_type=code&redirect_uri=${redirectUri}&client_id=${clientId}&scope=${scope}`;
};
// routes.tsx
export const router = createBrowserRouter([
{
path: '/',
children: [
{
path: '/',
element: <HomePage />,
},
{ path: 'redirect/oauth', element: <AuthPage /> },
...
],
},
]);
- 사용자는 github가 제공하는 로그인 페이지에서 github의 id와 pw를 제공하여 로그인을 한다.
5 ~ 12. Authorization Code 발급
- 로그인에 성공한 사용자는 쿼리스트링에 Authorization Code를 포함하여 redirectUri에 등록된 AuthPage로 이동한다.
- AuthPage 컴포넌트에서는 쿼리스트링에 포함된 Authorization Code를 api를 통해 백엔드 서버로 전송한다.
- 백엔드단에서는 전송받은 Authorizaion Code로 github에서 Access Token을 발급 받고 Access Token으로 github에서 사용자 정보를 받는다.
- 이후 백엔드는 프론트에게 JWT 토큰과 사용자 정보를 전송하고 프론트는 JWT 토큰과 사용자 정보를 localStorage에 저장하고 사용자 인증이 필요한 API 요청의 경우 헤더에 JWT 토큰을 담아 전송한다.
// AuthPage.tsx
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { API_URL } from '@constants/apis';
import { USER_INFO_KEY } from '@constants/login';
export const AuthPage = () => {
// useLocation을 사용해 현재 URL 정보를 획득
const location = useLocation();
// 로그인이 완료되면 HomePage로 이동하기 위한 navigate 선언
const navigate = useNavigate();
// new URLSearchParams를 사용하여 현재 URL의 쿼리 문자열에서 code 매개변수 값을 추출
const queryCode = new URLSearchParams(location.search).get('code');
// 백엔드에게 Authorization Code를 전송하고 JWT 토큰과
// 사용자 정보(id, 프로필 사진, 지역정보 등)를 받기 위한 함수
const getTokenAndUserInfo = async (code: string | null) => {
const response = await fetch(
`https://www.guardiansofthecodesquad.site/login/oauth/github?code=${code}`
);
const data = await response.json();
return data;
};
const runGetLoginUserInfoAPI = async () => {
const userInfoData = await getGithubLoginToken(queryCode);
if (!userInfoData) {
return;
}
const token = userInfoData.data.token;
const memberInfo = userInfoData.data.memberInfo;
// JWT 토큰과 사용자 정보를 localStorage에 저장
localStorage.setItem('loginToken', token);
localStorage.setItem(USER_INFO_KEY, JSON.stringify(memberInfo));
// 모든 작업 완료 후 HomePage로 이동
navigate('/')
}
useEffect(() => {
runGetLoginUserInfoAPI();
}, []);
return <></>;
};
// JWT 토큰을 헤더에 담아 사용자 인증을 하는 예시
// 사용자가 선택한 지역을 사용자 정보에 PUT 하는 함수
export const putUserLocation = async (
mainLocationIdx: number | null | undefined,
subLocationIdx: number | null | undefined
) => {
try {
const token = localStorage.getItem('loginToken');
const response = await fetch(`${API_URL}/location`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
main: mainLocationIdx,
sub: subLocationIdx,
}),
});
const userLocationData = await response.json();
if (!response.ok) {
throw new Error(ERROR_MESSAGE['UNDEFINED']);
}
return userLocationData.data;
} catch (error) {
return error;
}
};
배운 점과 느낀 점
- OAuth의 개념과 기본 용어부터 학습할 내용이 많았지만, 정확하게 이해하고 넘어가기 위해 프로젝트 기간 중 시간을 많이 투자했던 기능이었다.
- 기능 구현 중 어려운 부분이 있었지만, OAuth 로그인 구현 경험이 있던 프론트엔드 동료 스눕과 백엔드 동료 로이, 고뭉남 덕분에 github OAuth 로그인 기능을 구현해서 기뻤고, 함께해준 동료들에게 감사의 인사를 전하고 싶다🙇♂️
학습 단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 정정하도록 하겠습니다.
참고 : https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
https://rrecoder.tistory.com/148
https://blog.naver.com/mds_datasecurity/222182943542
https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C
https://barbera.tistory.com/62
https://velog.io/@chuu1019/%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-JWTJson-Web-Token
'프로젝트 > 세컨핸드' 카테고리의 다른 글
[기능 구현] 동적 portal 만들기 (0) | 2024.01.22 |
---|---|
[기능 구현] 커스텀 훅(Custom Hook)을 사용하여 비동기 로딩 및 에러 상태 처리 (0) | 2024.01.22 |
[Trouble Shooting] 상품 수정 등록 후 뒤로가기 경로 문제 (0) | 2023.11.23 |
[Trouble Shooting] 상품 수정시 이미지 파일 처리 (0) | 2023.09.21 |
[Trouble Shooting] 지역 설정 기능 구현 과정에서 겪은 문제 (0) | 2023.09.21 |