From f206efe4e7a50b04ae517b8a9eeaf04b855851a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8B=A4=EB=B9=88?= <991012dabin@gmail.com> Date: Fri, 19 Jan 2024 12:45:18 +0900 Subject: [PATCH 1/4] =?UTF-8?q?modify:=20=ED=95=9C=20=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=80=EC=9E=A5=ED=95=A0=EB=8F=84=EB=A1=9D=20set?= =?UTF-8?q?Cookies=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Login/LoginForm/index.tsx | 11 ++++------- src/types/auth.ts | 6 ++---- src/utils/lib/cookies.ts | 17 ++++------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/components/Login/LoginForm/index.tsx b/src/components/Login/LoginForm/index.tsx index 41c51058..e2000aa7 100644 --- a/src/components/Login/LoginForm/index.tsx +++ b/src/components/Login/LoginForm/index.tsx @@ -33,13 +33,10 @@ const LoginForm = () => { ) => { event.preventDefault(); const response = await postLogin(formData); - setCookies( - response.name, - response.email, - response.access_token, - response.refresh_token, - response.expires_in - ); + setCookies('userName', response.name, response.expires_in); + setCookies('userEmail', response.email, response.expires_in); + setCookies('accessToken', response.access_token, response.expires_in); + setCookies('refreshToken', response.refresh_token, response.expires_in); }; return ( diff --git a/src/types/auth.ts b/src/types/auth.ts index 65678c83..49861aba 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -36,10 +36,8 @@ export type LoginData = { }; export type SetCookies = ( - userName: string, - userEmail: string, - accessToken: string, - refreshToken: string, + name: string, + value: string, expiresIn: number ) => void; diff --git a/src/utils/lib/cookies.ts b/src/utils/lib/cookies.ts index 9e992d04..03896659 100644 --- a/src/utils/lib/cookies.ts +++ b/src/utils/lib/cookies.ts @@ -1,21 +1,12 @@ import { SetCookies, GetCookies } from '@/types/auth'; -export const setCookies: SetCookies = ( - userName, - userEmail, - accessToken, - refreshToken, - expiresIn -) => { +export const setCookies: SetCookies = (name, value, expiresIn) => { try { - document.cookie = `userName=${userName};max-age=${expiresIn};path=/;secure`; - document.cookie = `userEmail=${userEmail};max-age=${expiresIn};path=/;secure`; - document.cookie = `accessToken=${accessToken};max-age=${expiresIn};path=/;secure`; - document.cookie = `refreshToken=${refreshToken};max-age=${expiresIn};path=/;secure`; - console.log('쿠키설정 성공'); + document.cookie = `${name}=${value};max-age=${expiresIn};path=/;secure`; + console.log(`${name}: 쿠키설정 성공`); } catch (error) { console.log(error); - alert('쿠키설정에 실패했습니다.'); + alert(`${name}: 쿠키설정에 실패했습니다.`); } }; From 3b13fd87a3054c1cc576370b5846c46341758754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8B=A4=EB=B9=88?= <991012dabin@gmail.com> Date: Fri, 19 Jan 2024 16:17:16 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20axios=20response=20interceptor=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/lib/instance.ts | 57 ++++++++++++++++++++++++++++++--- src/api/lib/postRefreshToken.ts | 14 ++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/api/lib/postRefreshToken.ts diff --git a/src/api/lib/instance.ts b/src/api/lib/instance.ts index 91d77121..e0a7ee6a 100644 --- a/src/api/lib/instance.ts +++ b/src/api/lib/instance.ts @@ -1,5 +1,6 @@ -import { getCookies } from '@utils/lib/cookies'; +import { getCookies, setCookies } from '@utils/lib/cookies'; import axios from 'axios'; +import postRefreshToken from './postRefreshToken'; const instance = axios.create({ baseURL: import.meta.env.VITE_SERVER_URL, @@ -10,12 +11,14 @@ const instance = axios.create({ // 요청 인터셉터 추가 instance.interceptors.request.use( - (config: any): any => { + config => { const accessToken = getCookies('accessToken'); if (accessToken) { config.headers['Authorization'] = `Bearer ${accessToken}`; } + + return config; }, error => { return Promise.reject(error); @@ -23,8 +26,52 @@ instance.interceptors.request.use( ); // TODO : 응답 인터셉터 추가 -instance.interceptors.response.use((response: any): any => { - return response; -}); +instance.interceptors.response.use( + response => { + return response; + }, + async error => { + const { + config, + response: { status } + } = error; + + // 액세스 토큰이 만료되었을 때 + if (status === 401) { + if (error.response.data.code === 'JWT_EXPIRED_AUTHORIZATION') { + const originalRequest = config; + + // 토큰 리프레시 요청 + const response = await postRefreshToken(); + + // 새로운 토큰 저장 + if (response.status === 200) { + const { + access_token: newAccessToken, + refresh_token: newRefreshToken, + expires_in: expiresIn + } = response.data; + setCookies('accessToken', newAccessToken, expiresIn); + setCookies('refreshToken', newRefreshToken, expiresIn); + + // 401로 요청 실패했던 요청 새로운 accessToken으로 재요청 + originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + return axios(originalRequest); + + // 리프레시 토큰도 만료되었을 때 = 재로그인 안내 + } else if (response.status === 404) { + console.log(response.data.message); + alert('다시 로그인해주십시오.'); + window.location.replace('/login'); + } + } + console.log(error.response.data.code, error.response); + alert('서비스 이용에 불편을 드려 죄송합니다.'); + window.location.replace('/login'); + } + console.log('response error : ', error); + return Promise.reject(error); + } +); export default instance; diff --git a/src/api/lib/postRefreshToken.ts b/src/api/lib/postRefreshToken.ts new file mode 100644 index 00000000..91f85b06 --- /dev/null +++ b/src/api/lib/postRefreshToken.ts @@ -0,0 +1,14 @@ +import { getCookies } from '@utils/lib/cookies'; +import { instance } from '..'; + +const postRefreshToken = async () => { + const refreshToken = getCookies('refreshToken'); + + const response = await instance.post('/v1/member/refresh', { + refresh_token: refreshToken + }); + + return response; +}; + +export default postRefreshToken; From 5053a4ab364d14d6e1f480d5df67745eaa218271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8B=A4=EB=B9=88?= <991012dabin@gmail.com> Date: Fri, 19 Jan 2024 16:38:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?modify:=20postRefreshToken=20export=20?= =?UTF-8?q?=EB=B0=8F=20import=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 1 + src/api/lib/instance.ts | 5 +++-- src/components/Login/LoginForm/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 1eaf0d6d..c1cda60e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,3 +1,4 @@ export { default as instance } from './lib/instance'; export { default as getExample } from './lib/getExample'; export { default as postLogin } from './lib/postLogin'; +export { default as postRefreshToken } from './lib/postRefreshToken'; diff --git a/src/api/lib/instance.ts b/src/api/lib/instance.ts index e0a7ee6a..09cef17f 100644 --- a/src/api/lib/instance.ts +++ b/src/api/lib/instance.ts @@ -1,6 +1,7 @@ -import { getCookies, setCookies } from '@utils/lib/cookies'; import axios from 'axios'; -import postRefreshToken from './postRefreshToken'; + +import { getCookies, setCookies } from '@utils/lib/cookies'; +import { postRefreshToken } from '..'; const instance = axios.create({ baseURL: import.meta.env.VITE_SERVER_URL, diff --git a/src/components/Login/LoginForm/index.tsx b/src/components/Login/LoginForm/index.tsx index e2000aa7..e4ae9a76 100644 --- a/src/components/Login/LoginForm/index.tsx +++ b/src/components/Login/LoginForm/index.tsx @@ -16,8 +16,8 @@ const LoginForm = () => { // HACK : 추후 useState로 입력 데이터 관리할 예쩡 const formData: LoginData = { - email: 'mary0393@naver.com', - password: 'qqqq1111!' + email: 'juhwanTest@gmail.com', + password: 'juhwanTest' }; // HACK: 유효성 검사 기능 구현 후 유효성 메세지 노출 여부 결정 From 929b443634212f66a8739f6e2631c1d86cf5253f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8B=A4=EB=B9=88?= <991012dabin@gmail.com> Date: Fri, 19 Jan 2024 19:20:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?axios=20interceptor=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/lib/instance.ts | 120 +++++++++++++++++++++------------------- src/types/axios.ts | 15 +++++ 2 files changed, 77 insertions(+), 58 deletions(-) create mode 100644 src/types/axios.ts diff --git a/src/api/lib/instance.ts b/src/api/lib/instance.ts index 09cef17f..8053eb97 100644 --- a/src/api/lib/instance.ts +++ b/src/api/lib/instance.ts @@ -1,7 +1,12 @@ -import axios from 'axios'; +import axios, { + AxiosError, + AxiosResponse, + InternalAxiosRequestConfig +} from 'axios'; import { getCookies, setCookies } from '@utils/lib/cookies'; import { postRefreshToken } from '..'; +import { AxiosResponseError } from '@/types/axios'; const instance = axios.create({ baseURL: import.meta.env.VITE_SERVER_URL, @@ -10,69 +15,68 @@ const instance = axios.create({ } }); -// 요청 인터셉터 추가 -instance.interceptors.request.use( - config => { - const accessToken = getCookies('accessToken'); - - if (accessToken) { - config.headers['Authorization'] = `Bearer ${accessToken}`; - } +const onRequest = ( + config: InternalAxiosRequestConfig +): InternalAxiosRequestConfig => { + const accessToken = getCookies('accessToken'); - return config; - }, - error => { - return Promise.reject(error); + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}`; } -); -// TODO : 응답 인터셉터 추가 -instance.interceptors.response.use( - response => { - return response; - }, - async error => { - const { - config, - response: { status } - } = error; - - // 액세스 토큰이 만료되었을 때 - if (status === 401) { - if (error.response.data.code === 'JWT_EXPIRED_AUTHORIZATION') { - const originalRequest = config; - - // 토큰 리프레시 요청 - const response = await postRefreshToken(); - - // 새로운 토큰 저장 - if (response.status === 200) { - const { - access_token: newAccessToken, - refresh_token: newRefreshToken, - expires_in: expiresIn - } = response.data; - setCookies('accessToken', newAccessToken, expiresIn); - setCookies('refreshToken', newRefreshToken, expiresIn); - - // 401로 요청 실패했던 요청 새로운 accessToken으로 재요청 - originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; - return axios(originalRequest); - - // 리프레시 토큰도 만료되었을 때 = 재로그인 안내 - } else if (response.status === 404) { - console.log(response.data.message); - alert('다시 로그인해주십시오.'); - window.location.replace('/login'); - } + return config; +}; + +const onErrorRequest = (error: AxiosError | Error): Promise => { + return Promise.reject(error); +}; + +const onResponse = (response: AxiosResponse): AxiosResponse => { + return response; +}; + +const onErrorResponse = async (error: AxiosResponseError) => { + const { config, response } = error; + + // 액세스 토큰이 만료되었을 때 + if (response && response.status === 401) { + if (error.response.data.code === 'JWT_EXPIRED_AUTHORIZATION') { + const originalRequest = config; + + // 토큰 리프레시 요청 + const tokenResponse = await postRefreshToken(); + + // 새로운 토큰 저장 + if (tokenResponse.status === 200) { + const { + access_token: newAccessToken, + refresh_token: newRefreshToken, + expires_in: expiresIn + } = tokenResponse.data; + setCookies('accessToken', newAccessToken, expiresIn); + setCookies('refreshToken', newRefreshToken, expiresIn); + + // 401로 요청 실패했던 요청 새로운 accessToken으로 재요청 + originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + return instance(originalRequest); + + // 리프레시 토큰도 만료되었을 때 = 재로그인 안내 + } else if (tokenResponse.status === 404) { + console.log(tokenResponse.data.message); + return Promise.reject(error); } - console.log(error.response.data.code, error.response); - alert('서비스 이용에 불편을 드려 죄송합니다.'); - window.location.replace('/login'); } - console.log('response error : ', error); + console.log(error.response.data.code, error.response); return Promise.reject(error); } -); + console.log('response error : ', error); + return Promise.reject(error); +}; + +// 요청 인터셉터 추가 +instance.interceptors.request.use(onRequest, onErrorRequest); + +// TODO : 응답 인터셉터 추가 +instance.interceptors.response.use(onResponse, onErrorResponse); export default instance; diff --git a/src/types/axios.ts b/src/types/axios.ts new file mode 100644 index 00000000..be195153 --- /dev/null +++ b/src/types/axios.ts @@ -0,0 +1,15 @@ +import { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +export type ResponseData = { + code: T; + message: T; +}; + +export type ResponseError = AxiosResponse & { + data: ResponseData; +}; + +export type AxiosResponseError = { + config: InternalAxiosRequestConfig; + response: ResponseError; +};