예전부터 소셜 로그인
구현을 해보고싶었다. 또한 개인정보 유출 논란이 많은 요즘 직접 중요한 아이디, 비밀번호를 DB에 저장하고 관리하기 보다는 유저의 정보를 플랫폼에 의존하는게 서로에게 좋다고 생각했다.
ootdzip
은 사용자에게 가장 친숙한 세가지 플랫폼을 이용하기로 했다.
소셜 로그인에 없으면 안되는 존재, 사실상 모든 사용자의 휴대폰에 깔려있다고 보면 된다. 또한 유일하게 localhost:3000
로 리다이렉트를 걸어주기 때문에 dev
모드 개발 시 반드시 필요하다.
네이버 또한 우리 나라의 거의 모든 사용자가 아이디가 있기때문에 사용하게 되었다.
다른 플랫폼에 비해 계정을 만들기 쉽다는 단점이 있다.
앱스토어에 업로드하기 위해서는 애플 로그인이 있어야한다고 해서 사용하게 되었다.
구글은 웹뷰에서의 소셜 로그인을 보안 문제상 지원하지 않는다고 한다.
https://developers-kr.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html
해결 방법으로 Chrome Custom Tab으로 열거나 WebView의 userAgent를 수정해 우회하는 방법이 있지만 걸리면 앱이 정지, 삭제 된다고 해 넣지 않기로 했다.
직접 플랫폼 로그인 페이지로 라우팅
const NAVER_URI = `https://nidnaver.com/oauth2.0/authorize?response_type=code&client_id=${NEXT_PUBLIC_NAVER_CLIENT_ID}&
redirect_uri=${NEXT_PUBLIC_REDIRECT_URI}`;
router.replace(NAVER_URI);
로그인 페이지로 진입 시 플랫폼 쿼리 파라미터를 확인해 백엔드에서 리다이렉트
const NAVER_URI = `https://ootdzip.com/api/v1/login/authorization/naver';
router.push(NEXT_PUBLIC_NAVER_URI);
기존에는 프론트에서 모든 플랫폼에 대해 직접 라우팅 해 처리했지만
프론트에서 직접 처리하는 경우 백엔드의 처리 로직이 플랫폼 개수만큼 많아진다고 해 백엔드에서 관리하는걸로 변경했다.
유저의 동의를 받는다. 이때 받는 정보의 종류는 해당 플랫폼의 개발자 사이트에서 수정할 수 있다.
리다이렉트 페이지는 각 플랫폼의 개발자 페이지에서 관리할 수 있다. 리다이렉트 페이지의 쿼리 파라미터를 통해 code를 추출해 백엔드에 인증을 해야한다.
export interface QueryParams {
code: string;
}
const router = useRouter();
const { code } = router.query as QueryParams;
예시
http://localhost:3000/sign-in/kakao/callback?code=12345678901234safa121a
- code: 12345678901234safa121a
const { data } = await fetcher.get(
`v1/login/oauth/code/${payload.callback![0]}?code=${payload.code}`
);
jwt 토큰 저장소에 대해 많은 고민을 했다. 보통 앱의 경우 자동로그인 기능을 보통 지원하지 다시 로그인을 하는 경우는 많이 없기 때문이다. 이러한 점에서 나는 localStorage
를 사용하기로 결정했다.
// callback 페이지에서 사용하는 API
const login = async (payload: QueryParams) => {
try {
// 액세스 토큰을 받아온다.
const data = await authService.login(payload);
if (data.accessToken) {
// 로컬 스토리지에 담아준다.
localStorage.setItem('accessToken', data.accessToken
localStorage.setItem('refreshToken', data.refreshToken);
return true;
}
} catch (error) {
setError(error);
router.replace('/sign-in');
}
jwt 토큰은 크게 accessToken
과 refreshToken
으로 이루어져있다.
accessToken
이 만료 되었을 때 재발급을 위해 사용accessToken
이 만료가 되었다면 401 StatusCode/U002 DivisonCode가 발생한다. 에러가 발생하면 axios interceptor의 error response를 활용해 refreshToken
로 accessToken
을 재발급 받는다. 그리고 이전 요청을 다시 보낸다.
리프레시 토큰은 리프레시 토큰 로테이션방식을 통해 기한이 만료될일이 잘 없다.
자동로그인시 refreshToken
을 재발급 해 교체해준다. 이로써 리프레시 토큰의 유효기간이 계속 늘어난다.
한 페이지에서 액세스 토큰이 만료된 상태에서 여러개의 비동기 api 요청을 보냈을 때,
재요청된 토큰을 다음 api 요청에 사용하는 것이 아닌, 비동기 api 요청 개수만큼 refreshToken
을 재 발급해 한가지 요청을 제외한 refreshToken
은 유효하지 않는 오류가 발생
리프레시 중인지 여부를 추적하는 변수를 사용해
리프레이 요청중이라면 refreshToken
재발급을 하지 않도록 해 해결
let refreshing = false; // 리프레시 중인지 여부를 추적하는 변수
fetcher.interceptors.response.use(
(config) => {
return config;
},
async (error) => {
if (error.response.data.divisionCode === 'U002') {
if (!refreshing) {
// 리프레시 중이 아닌 경우에만 리프레시 요청 진행
refreshing = true; // 리프레시 중으로 표시
const { getNewToken } = PublicApi();
try {
await getNewToken();
refreshing = false; // 리프레시 완료 후 상태 변경
} catch {
localStorage.clear();
window.location.replace('/sign-in');
return ;
}
}
const accessToken = localStorage.getItem('accessToken');
const newConfig = error.config;
newConfig.headers.Authorization = `Bearer ${accessToken}`;
// 리프레시 중인 경우 요청 보류
return fetcher.request(newConfig);
}
}
);
백엔드에 소셜 로그인으로의 리다이렉트를 요청하는 경우 백엔드에서 redirect_url을 ootdzip.com
으로 고정해버려 localhost:3000
으로 이동할 수 없었다. 따라서 개발 단계에서의 변화를 눈으로 확인할 수 없었다.
redirect_url
을 함께 보내 해당 url로 이동하는걸로 해결했다.
그렇게 마무리 되는듯 하였으나 백엔드에서 리다이렉트를 관리하다보니 간편로그인을 적용시킬 수 없었고, 카카오톡 2차 인증 로직이 활성화 되어버렸다. 앱에서 카카오톡을 활용한 로그인이 아닌 직접 아이디 비밀번호를 입력하는 상황은 사용자에게 정말 안좋은 유저 경험을 안겨줄것 같았다.
카카오톡은 내가 직접 카카오 간편 로그인 함수를 사용해 해결하기로 했다.
오늘은 소셜 로그인과 jwt 토큰 핸들링에 대해 알아보았다.
소셜 로그인은 아이디 비밀번호를 일일히 기억을 안해도 된다는 점에서 유저에게 정말 좋은 경험이 될것같다!