이번 토이 프로젝트의 주된 의도 중 하나인 소셜 로그인을 해보기 위해 (이전 프로젝트에서 OAuth2와 Node openID를 써본 경험은 있지만) 클라이언트 사이드를 구축하였다.
우선, 소셜 로그인은 카카오톡의 카카오로그인 (https://developers.kakao.com/docs/latest/ko/kakaologin/common)을 활용하였다.
우선 기초 설계로써, 프론트엔드와 백엔드가 어느 부분 까지 담당하고, 데이터를 넘겨주고 받을 것인지 정하는 것이 중요하다.
어느 부분이 무엇인가?
위의 그림을 보면 세개의 도메인에 토큰을 주고받는 과정을 확인 할 수 있다. 가장 좌측 BusinessClient는 Frontend Client, OAuth Server는 엑세스토큰을 발급해주는 KakaoLogin, ResourceServer는 Backend Server가 될것이다.
이 그림을 보고 이해해야 할 부분은 클라이언트에서 1. 엑세스 코드 발급을 요청하고, 2. 저장하여, 3. Authorization이 들어간 Api에 헤더를 포함하여 요청한다는 것이다.
1. 엑세스 코드 발급
다양한 소셜로그인을 구현하다 보면 (네이버, 카카오, 구글 중 카카오와 구글을 이용해 보았다) 주고 받는 코드가 생각보다 많고, 처음엔 뭐가 뭔지 모르겠다는 느낌을 많이 받았다... (꼼꼼히 안 읽는 내 잘못...)
우선 카카오 로그인에는 엑세스토큰 (Access Token - 토큰과 코드가 많다...! 잘 구분하자)을 받기 위해서는 인가 코드 (Authorization Code)를 발급 받아야 한다. 이후 발급 받은 코드로 엑세스 토큰과 필요시 Refresh Token을 발급 받는다. 이 세개의 차이가 무엇일까?
그 전에 소셜 로그인을 왜 사용하는지도 다시 한번 돌아 볼 필요가 있다고 생각한다. 소셜 로그인은 다음과 같은 이점을 준다.
사용자의 입장
- 앱에 원활하게 엑세스 할 수 있도록 도와준다.
- 로그인 및 등록 과정을 단순화 한다.
- 인증과정을 거치지 않고 일정 기간 로그인 상태를 유지시켜 준다 (리소스에 접근 가능하다).
개발자의 입장
- 로그인 과정 간소화와 동시에 사용자의 데이터에 대한 안정적인 엑세스를 제공한다.
1. 인가 코드
인가 코드란 토큰을 발급 받기 위한 값이다. 카카오 로그인에서는 해당 서비스의 권한을 설정하는데, 해당 인가 코드는 그 권한에 알맞는 토큰을 발행한다.
2. 엑세스 토큰
사용자 정보 가져오기 같은 Api를 호출하는데 직접적으로 사용된다.
3. 리프레시 토큰
일정 기간 동안 사용자가 별다른 인증 수단 없이 엑세스 토큰을 발급 받을 수 있도록 한다.
결국 인가 코드는 엑세스 토큰의 접근 범위를 설정 및 발급 해주고, 리프레시 토큰은 유효성에 따라 엑세스 토큰을 재발급 해준다.
엑세스 토큰의 생명 주기가 길다면, 노출에 더욱 취약해진다. 같은 엑세스 토큰을 오랫동안 사용하면 악의적인 유저가 사용자의 정보를 쉽게 접근 할 수 있기 때문이다.
따라서 리프레시 토큰을 활용하여 엑세스 토큰의 주기성은 짧게, 리프레시 토큰은 길게 설정하여, 사용자가 리프레시 토큰을 보유하고, 유효 할 때, 별도의 로그인 절차 없이 엑세스 토큰을 발급하며, 로그인 상태를 유지해주는 것이다. 이때 리프레시 토큰도 함께 재발급 받아 주기가 비교적 긴 리프레시 토큰 또한 최신화한다.
이러한 과정들 중, Frontend는 인가 코드를 발급 받아 엑세스 토큰까지 직접 저장할 수 있다. 또한 서버에 요청하여 이러한 일렬의 과정들을 모두 위임 할 수도 있다. 해당 프로젝트는 후자를 택했다. 아무래도 직접적인 로직은 모두 서버로 넘어가는 추세기도 하고, 엑세스 토큰을 발급받는 과정까지 서버사이드에서 해결하는 것이 더 안정적이라고 판단했다.
사실 장황했지만, 이 프로젝트에서 프론트 클라이언트가 하는 것은 많이 없다... 쿠키가 있는지 서버 초기 로딩 시 확인하고, 소셜 로그인 버튼을 서버 Api를 호출 하도록 한 후, Response로 오는 엑세스 토큰을 Redux 스토어에 등록하는 것 뿐이다... 하지만 기초 지식 없이 (백이든 프론트든...) 겁 없이 들이댔다가 모두 삽질 한 경험이 있기에 어느 누군가는 (둘다 잘 아는게 best지만) 알아야 할 것이다.
Main Page
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { getUser } from "../../api/user";
import { useNavigate } from "react-router-dom";
import { authActions } from "../../store";
const MainPage = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const accessToken = useSelector(state => state.accessToken);
const [userInfo, setUserInfo] = useState({nickname: '', email: ''});
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(()=>{
if(accessToken!==''){
getUser('1', accessToken).then((res) => {
setUserInfo(prevState => {return {...prevState, nickname: res.data.nickname, email: res.data.email}})
setIsLoggedIn(true);
})
}
},[accessToken])
return (<div>
<h1>안녕하세요</h1>
{isLoggedIn ||
<>
<h1>로그인을 해주세요!</h1>
<button onClick={()=>{navigate('/login')}}>로그인</button>
</>
}
{isLoggedIn &&
<>
<h1>
{userInfo.nickname}님!
</h1>
<button onClick={()=>{
dispatch(authActions.logout())
setIsLoggedIn(false);
}}>로그아웃</button>
</>
}
</div>)
}
메인 페이지는 다음과 같이 구성했다. 토큰의 유무에 따라 로직이 구성되었고, 유효한 토큰인지 확인하고 에러 핸들링을 추가 해야 할 것이다.
Login Page
import React from 'react';
import './index.css'
import { Link } from "react-router-dom";
import kakaoBtn from '../../assets/img/kakao_login_large_wide.png'
const LoginPage = () =>{
return (
<div className='loginPage'>
<h1>WebSocket Chrome Extension Test Front Page</h1>
<input className='input_holder' type='text' placeholder='아이디를 입력해주세요' />
<br />
<br />
<input className='input_holder' type='password' placeholder='비밀번호를 입력해주세요'/>
<br />
<br />
<button className='login_btn'>로그인</button>
<br />
<a href={'http://localhost:8080/oauth2/authorization/kakao'}><img className='login_btn_kakao' src={kakaoBtn} /></a>
<br />
<div className='login_extra'>
<Link to='/signup'>회원가입</Link> / <a>아이디 찾기</a> / <a>비밀번호 찾기</a>
</div>
</div>
)
}
export default LoginPage
로그인 페이지는 다음과 같이 구성되있다. 로그인 시 서버의 Api를 호출한다. Api 호출 시, 서버에서 카카오 로그인 페이지로 리다이렉트 하고, 받은 인가 코드를 활용하여 엑세스코드 발급, 그 후 리다이렉팅 한다.
Redirect Page
import React, { useEffect } from "react";
import { getUserToken } from "../../api/sign";
import { useDispatch } from 'react-redux';
import { authActions } from '../../store/index';
import { useNavigate } from 'react-router-dom';
const RedirectPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const params = new URLSearchParams(window.location.search)
getUserToken(params.get('code'), params.get('state')).then((res) => dispatch(authActions.login(res)));
useEffect(()=>{
navigate('/');
},[])
return
}
export default RedirectPage;
서버가 리다이렉트를 시키고, 해당 라우트로 접근할 때 받게 되는 컴포넌트이다. URLSearchParams 를 이용하여 받은 리다이렉트 URL 파라미터를 서버 Api에 활용해준다. 이때, React.StrictMode를 키고 개발을 하게 되면, 렌더링이 두번 일어나는데, 이는 오류를 초례한다. 인가코드를 발급 받은 후, 엑세스 코드를 발급 받는 과정은 당연히 비동기적으로 일어나고, 렌더링이 두번 일어나는 동안 두개의 인가코드가 발급, 해당 엑세스 토큰은 활용 불가해지기 때문이다. 따라서 React.StrictMode는 끄고 진행하였다.
'개발일지' 카테고리의 다른 글
[크롬 익스텐션: 05] StompJs & SockJs, 디자인 및 구현 - 1 (0) | 2022.06.23 |
---|---|
[리펙토링: 나들서울] Debounce & Throttling (0) | 2022.06.14 |
[크롬 익스텐션: 03] sockjs-client를 이용한 소켓 통신 (1) | 2022.06.05 |
[크롬 익스텐션: 02] Manifest V3 (0) | 2022.06.03 |
[크롬 익스텐션: 01] Chrome Extension(Manifest V3)xSocket 통신 (0) | 2022.06.03 |