From 1881429390aad04a0eba5561156c263a25c00d3c Mon Sep 17 00:00:00 2001 From: giho Date: Tue, 16 Jul 2024 00:38:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20accessToken=20=EC=9E=AC=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/instances/index.ts | 58 ++++++++++++++----- src/apis/services/auth.ts | 21 +++---- src/apis/services/user.ts | 1 - src/pages/Signin/SignInPage.tsx | 14 +++++ .../_components/GoogleLoginButton.hook.tsx | 31 +++++----- src/store/useAuthStore.ts | 39 +++++++++---- src/utils/getAccessToken.ts | 1 + 7 files changed, 110 insertions(+), 55 deletions(-) diff --git a/src/apis/instances/index.ts b/src/apis/instances/index.ts index 45b3cdd..6322f35 100644 --- a/src/apis/instances/index.ts +++ b/src/apis/instances/index.ts @@ -1,8 +1,8 @@ import axios, { AxiosError } from 'axios'; import { handleAxiosError } from '../../utils/handleAxiosError'; import { handleUnexpectedError } from '../../utils/handleUnexpectedError'; -import getAccessToken from '../../utils/getAccessToken'; import { renewTokens } from '../services/auth'; +import { useApplicationAuthTokenStore } from '@/store/useAuthStore'; const baseURL = process.env.API_URL; @@ -13,6 +13,7 @@ export const axiosInstance = axios.create({ headers: { 'Content-Type': 'application/json', }, + withCredentials: true, }); export const axiosInstanceWithToken = axios.create({ @@ -27,10 +28,7 @@ export const axiosInstanceWithToken = axios.create({ // request interceptor의 경우 token을 넣을 때 자주 사용한다. axiosInstanceWithToken.interceptors.request.use( (config) => { - // 토큰을 가져온다. useApplicationAuthTokenStore()는 - // hook을 사용하는 것이기 때문에 .getState()를 사용한다. - const accessToken = getAccessToken(); - console.log('엑세스 토큰', accessToken); + const { accessToken } = useApplicationAuthTokenStore.getState(); // 만약 토큰이 존재하는 경우 헤더에 넣어준다. if (accessToken) { @@ -39,28 +37,58 @@ axiosInstanceWithToken.interceptors.request.use( return config; }, (error) => { + console.error(error); return Promise.reject(error); }, ); axiosInstanceWithToken.interceptors.response.use( - (response) => { - return response; - }, + (response) => response, async (error: AxiosError) => { if (!axios.isAxiosError(error)) { handleUnexpectedError(error); return Promise.reject(error); } - // accessToken 만료시 재발급 - if (error.response?.status === 401) { + // 토큰 관리 + try { + const { setAccessToken } = useApplicationAuthTokenStore.getState(); const originalRequest = error.config; - await renewTokens(); - const accessToken = getAccessToken(); - if (accessToken && originalRequest) { - originalRequest.headers['authorization'] = `Bearer ${accessToken}`; - return axiosInstanceWithToken(originalRequest); + + // 1. accessToken이 없거나 만료된 경우 + if (error.response?.status === 401) { + // accessToken 갱신 + const newAccessToken = await renewTokens(); + if (typeof newAccessToken === 'string') { + // 상태 업데이트 후 새로운 accessToken 값 가져오기 + return new Promise((resolve) => { + // Promise로 subscribe를 사용해 accessToken이 업데이트 되었을 때까지 기다림 + const unsubscribe = useApplicationAuthTokenStore.subscribe((state) => { + // 비동기적으로 accessToken이 업데이트 되면 조건문을 만족하게 됨 + if (state.accessToken === newAccessToken) { + // 일단 unsubscribe + unsubscribe(); + if (originalRequest) { + // 새로운 accessToken으로 요청 재시도 + originalRequest.headers['authorization'] = `Bearer ${state.accessToken}`; + resolve(axiosInstanceWithToken(originalRequest)); + } + } + }); + // 비동기적으로 accessToken 업데이트 + setAccessToken(newAccessToken); + }); + } + } + } catch (error: unknown) { + if (!axios.isAxiosError(error)) { + handleUnexpectedError(error); + return Promise.reject(error); + } + // 2. accessToken을 새로 요청했을 때 refreshToken이 만료되거나 잘못된 경우 로그아웃 처리 + if (error.response?.status === 401) { + window.location.href = '/signIn'; + return null; } } diff --git a/src/apis/services/auth.ts b/src/apis/services/auth.ts index f48dff0..11fee75 100644 --- a/src/apis/services/auth.ts +++ b/src/apis/services/auth.ts @@ -1,19 +1,16 @@ -import axios from 'axios'; -import { axiosInstanceWithToken } from '../instances'; +import { axiosInstance } from '../instances'; + +interface RenewTokenResponse { + applicationAccessToken: string; +} // accessToken 만료시 token들을 재발급 export const renewTokens = async () => { try { - const response = await axiosInstanceWithToken.post('/auth/renewTokens'); - return response.data; + const response = await axiosInstance.post('/auth/renewTokens'); + return response.data.applicationAccessToken; } catch (error) { - if (!axios.isAxiosError(error)) { - console.error('에러가 발생했습니다.'); - return; - } - if (error.response?.status === 401) { - console.error('리프레시 토큰이 만료됐습니다. 다시 로그인해주세요.'); - window.location.href = '/signIn'; - } + console.error(error); + return error; } }; diff --git a/src/apis/services/user.ts b/src/apis/services/user.ts index ddaf8bf..a1f7d02 100644 --- a/src/apis/services/user.ts +++ b/src/apis/services/user.ts @@ -34,7 +34,6 @@ export const getMyUser = async (userId: string) => { export const getMyOwnChannels = async (userId: string) => { try { const response = await axiosInstanceWithToken.get(`/users/myOwnChannels/${userId}`); - console.log(response.data); return response.data; } catch (e) { console.error(e); diff --git a/src/pages/Signin/SignInPage.tsx b/src/pages/Signin/SignInPage.tsx index 6c27322..964ae7f 100644 --- a/src/pages/Signin/SignInPage.tsx +++ b/src/pages/Signin/SignInPage.tsx @@ -7,10 +7,15 @@ import useSignInPage from './SignInPage.hook'; // components import { GoogleOAuthProvider } from '@react-oauth/google'; import GoogleLoginButton from './_components/GoogleLoginButton'; +import { useApplicationAuthTokenStore, useGoogleOAuthTokenStore } from '@/store/useAuthStore'; +import { useUserStore } from '@/store/useUserStore'; export default function SignInPage() { // logics const { CLIENT_ID } = useSignInPage(); + const accessToken = useApplicationAuthTokenStore.getState(); + const googleOAuthToken = useGoogleOAuthTokenStore.getState(); + const user = useUserStore.getState(); // view if (!CLIENT_ID) { @@ -21,6 +26,15 @@ export default function SignInPage() {
간단하게 로그인 또는 회원가입하세요
+ diff --git a/src/pages/Signin/_components/GoogleLoginButton.hook.tsx b/src/pages/Signin/_components/GoogleLoginButton.hook.tsx index b2e8f27..30b4237 100644 --- a/src/pages/Signin/_components/GoogleLoginButton.hook.tsx +++ b/src/pages/Signin/_components/GoogleLoginButton.hook.tsx @@ -1,34 +1,27 @@ // libraries -import { axiosInstanceWithToken } from '../../../apis/instances'; +import { axiosInstance } from '../../../apis/instances'; // hooks import { useNavigate } from 'react-router-dom'; import { useGoogleLogin } from '@react-oauth/google'; import { useApplicationAuthTokenStore, useGoogleOAuthTokenStore } from '../../../store/useAuthStore'; import { useUserStore } from '../../../store/useUserStore'; +import { useEffect } from 'react'; export default function useGoogleLoginButton() { const navigate = useNavigate(); - const { setAccessToken } = useApplicationAuthTokenStore(); - const { setGoogleOAuthToken } = useGoogleOAuthTokenStore(); - const { setUser } = useUserStore(); + const { accessToken, setAccessToken } = useApplicationAuthTokenStore.getState(); + const { googleOAuthToken, setGoogleOAuthToken } = useGoogleOAuthTokenStore.getState(); + const { user, setUser } = useUserStore(); const login = useGoogleLogin({ scope: 'email profile', onSuccess: async ({ code }) => { try { - await axiosInstanceWithToken.post('/auth/google/callback', { code }).then((response) => { - setAccessToken(response.data.applicationToken.accessToken); - setGoogleOAuthToken(response.data.googleToken.googleAccessToken); + await axiosInstance.post('/auth/google/callback', { code }).then((response) => { + console.log(response.data); + setAccessToken(response.data.applicationAccessToken); + setGoogleOAuthToken(response.data.googleAccessToken); setUser(response.data.user); - - // refresh token의 경우 백엔드에서 cookie로 보내주기 때문에 따로 저장할 필요가 없다. - if ( - response.data.user && - response.data.applicationToken.accessToken && - response.data.googleToken.googleAccessToken - ) { - navigate('/main'); - } }); } catch (error) { console.error(error); @@ -40,5 +33,11 @@ export default function useGoogleLoginButton() { flow: 'auth-code', }); + useEffect(() => { + if (accessToken && googleOAuthToken && user) { + navigate('/main'); + } + }, [accessToken, googleOAuthToken, user]); + return { login }; } diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts index ae0e795..48ad3df 100644 --- a/src/store/useAuthStore.ts +++ b/src/store/useAuthStore.ts @@ -1,8 +1,9 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; interface ApplicationAuthTokenStore { accessToken: string | null; - setAccessToken: (accessToken: string) => void; + setAccessToken: (accessToken: string | undefined) => void; removeAccessToken: () => void; } @@ -12,14 +13,30 @@ interface GoogleOAuthTokenStore { removeGoogleOAuthToken: () => void; } -export const useApplicationAuthTokenStore = create((set) => ({ - accessToken: null, - setAccessToken: (accessToken: string) => set({ accessToken }), - removeAccessToken: () => set({ accessToken: null }), -})); +export const useApplicationAuthTokenStore = create( + persist( + (set) => ({ + accessToken: null, + setAccessToken: (accessToken: string | undefined) => set({ accessToken }), + removeAccessToken: () => set({ accessToken: null }), + }), + { + name: 'applicationAuthTokenStore', + getStorage: () => localStorage, + }, + ), +); -export const useGoogleOAuthTokenStore = create((set) => ({ - googleOAuthToken: null, - setGoogleOAuthToken: (googleOAuthToken: string) => set({ googleOAuthToken }), - removeGoogleOAuthToken: () => set({ googleOAuthToken: null }), -})); +export const useGoogleOAuthTokenStore = create( + persist( + (set) => ({ + googleOAuthToken: null, + setGoogleOAuthToken: (googleOAuthToken: string) => set({ googleOAuthToken }), + removeGoogleOAuthToken: () => set({ googleOAuthToken: null }), + }), + { + name: 'googleOAuthTokenStore', + getStorage: () => localStorage, + }, + ), +); diff --git a/src/utils/getAccessToken.ts b/src/utils/getAccessToken.ts index f889d1d..db45d3c 100644 --- a/src/utils/getAccessToken.ts +++ b/src/utils/getAccessToken.ts @@ -2,6 +2,7 @@ import { useApplicationAuthTokenStore } from '../store/useAuthStore'; const getAccessToken = () => { const { accessToken } = useApplicationAuthTokenStore.getState(); + console.log(accessToken); return accessToken; }; From 71c588840168a17b796cb58ba8f5121e4a3ceef3 Mon Sep 17 00:00:00 2001 From: giho Date: Tue, 16 Jul 2024 02:47:42 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=9E=90=EC=9E=98=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/instances/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/apis/instances/index.ts b/src/apis/instances/index.ts index 6322f35..b714868 100644 --- a/src/apis/instances/index.ts +++ b/src/apis/instances/index.ts @@ -27,14 +27,14 @@ export const axiosInstanceWithToken = axios.create({ // request interceptor의 경우 token을 넣을 때 자주 사용한다. axiosInstanceWithToken.interceptors.request.use( - (config) => { + (AxiosRequestConfig) => { const { accessToken } = useApplicationAuthTokenStore.getState(); // 만약 토큰이 존재하는 경우 헤더에 넣어준다. if (accessToken) { - config.headers['authorization'] = `Bearer ${accessToken}`; + AxiosRequestConfig.headers['authorization'] = `Bearer ${accessToken}`; } - return config; + return AxiosRequestConfig; }, (error) => { console.error(error); @@ -61,7 +61,7 @@ axiosInstanceWithToken.interceptors.response.use( const newAccessToken = await renewTokens(); if (typeof newAccessToken === 'string') { // 상태 업데이트 후 새로운 accessToken 값 가져오기 - return new Promise((resolve) => { + return new Promise((resolve, reject) => { // Promise로 subscribe를 사용해 accessToken이 업데이트 되었을 때까지 기다림 const unsubscribe = useApplicationAuthTokenStore.subscribe((state) => { // 비동기적으로 accessToken이 업데이트 되면 조건문을 만족하게 됨 @@ -72,6 +72,8 @@ axiosInstanceWithToken.interceptors.response.use( // 새로운 accessToken으로 요청 재시도 originalRequest.headers['authorization'] = `Bearer ${state.accessToken}`; resolve(axiosInstanceWithToken(originalRequest)); + } else { + reject(new Error('Original request is missing')); } } });