일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- SSR
- autosize
- CORS
- CI/CD
- async/await
- 리터럴 타입
- 인터섹션
- 투포인터
- recoil
- 공변성
- React
- 호이스팅
- Promise
- useAppDispatch
- webpack
- dfs
- 이분 검색
- TS
- 태그된 유니온
- 결정 알고리즘
- tailwind
- Cypress
- 반공변성
- app router
- ESlint
- map
- Jest
- 무한 스크롤
- RTK Query
- 타입 좁히기
Archives
- Today
- Total
짧은코딩
axios interceptors를 활용하여 JWT 관리하기 with MSW2 본문
반응형
JWT 관리 방식
이 프로젝트에서는 권한 문제가 많기 때문에 JWT 관리를 어떻게 할지 더 고민했다.
이 글을 보고 accessToken은 메모리에 두고, refreshToken은 쿠키에 저장하는 방식으로 했다.
MSW2 설정
-로그인과 accessToken, refreshToken 재발급 api
http.post('/members/refresh', () => {
return HttpResponse.json(
{ accessToken: 'MSW-new-accessToken' },
{
headers: {
'Set-Cookie': 'refreshToken=MSW-refreshToken;Max-Age=999999999999;',
},
status: 200,
}
);
}),
http.post('/members/login', (info) => {
const infos: LoginInfo = info.request.json();
if (infos.password === '1234') {
return HttpResponse.json({ httpStatus: 'CONFLICT', message: '비밀번호가 틀립니다.' });
}
const position = infos.email === 'company@gmail.com' ? 'COMPANY' : 'INDIVIDUAL';
const data: GetUserInfo = {
id: faker.number.int(),
image: null,
email: infos.email,
name: '권오현',
nickname: '오리',
position: position,
accessToken: 'MSW-accessToken',
};
return HttpResponse.json(data, {
headers: {
'Set-Cookie': 'refreshToken=MSW-refreshToken;Max-Age=999999999999;',
},
});
}),
- 로그인(/members/login)
- 우선 로그인 mock api에서는 로그인을 하면 유저 정보, accessToken를 보내주고 cookie에 refreshToken을 설정했다.
- refresh(/members/refresh)
- refresh mock api에서는 새로운 accessToken과 refreshToken을 보내줬다.
-제품 리스트를 조회하는데 accessToken, refreshToken이 만료된 경우
import { DefaultBodyType, http, HttpResponse, StrictRequest } from 'msw';
export const checkAuthorization = ({
cookies,
request,
}: {
cookies: Record<string, string>;
request: StrictRequest<DefaultBodyType>;
}) => {
const { refreshToken } = cookies;
if (refreshToken !== 'MSW-refreshToken') {
return new HttpResponse(null, {
status: 410,
});
}
const accessToken = request.headers.get('authorization')?.slice(7) ?? '';
if (accessToken !== 'MSW-new-accessToken') {
return new HttpResponse(null, {
status: 409,
});
}
// 인증이 성공하면 null을 반환
return null;
};
checkAuthorization 함수는 accessToken, refreshToken을 체크해주는 함수이다.
import { http, HttpResponse } from 'msw';
import { faker } from '@faker-js/faker';
import { createProduct } from '@/mocks/api/data/product';
import { checkAuthorization } from '@/mocks/api/common.ts';
export const product = [
http.get('/products', async ({ cookies, request }) => {
// 함수 호출
const authCheckResult = checkAuthorization({ cookies, request });
// 만약 인증에 실패한 경우
if (authCheckResult !== null) {
return authCheckResult;
}
const products = faker.helpers.multiple(createProduct, {
count: faker.number.int({ min: 0, max: 5 }),
});
for (let i = 0; i < products.length; i++) {
products[i].indexNum = i + 1;
}
return HttpResponse.json(products);
}),
];
- refreshToken이 만료된 경우
- refreshToken이 만료되면 410 상태 코드를 보내준다.
- accessToken이 만료된 경우
- header에 들어있는 accessToken이 만료되면 409 상태 코드를 보내준다.
axios interceptors 설정
-초기 설정
import axios from 'axios';
let accessToken: string;
export const instance = axios.create({
withCredentials: true,
});
export async function reissuanceJwt() {
const res = await axios.post(`/members/refresh`, null, { withCredentials: true });
return res;
}
- accessToken 변수 메모리에 accessToken을 저장하는 방식으로 설정
- instance에 axios.create를 이용하여 커스텀
- withCredentials: true를 하여 쿠키를 공유
- reissuanceJwt 함수
- accessToken이 만료되면 reissuanceJwt 함수를 사용하여 갱신
-응답 처리 interceptors
instance.interceptors.response.use(
// 200번대 응답이 올때 처리
(response) => {
return response;
},
// 200번대 응답이 아닐 경우 처리
async (error) => {
const {
config,
response: { status },
} = error;
//토큰이 만료되을 때
if (status === 409) {
const originRequest = config;
const response = await reissuanceJwt();
//리프레시 토큰 요청이 성공할 때
if (response.status === 200) {
const newAccessToken = response.data.accessToken;
accessToken = newAccessToken;
//진행중이던 요청 이어서하기
originRequest.headers.Authorization = `Access=${newAccessToken}`;
return axios(originRequest);
//리프레시 토큰 요청이 실패할때(리프레시 토큰도 만료되었을때 = 재로그인 안내)
}
} else if (status === 410) {
sessionStorage.removeItem('userInfo');
window.location.replace('/login');
}
return Promise.reject(error);
}
);
- 200번대가 오면 그냥 그대로 처리
- error 발생 시
- 409 에러
- accessToken이 만료됐는데 refreshToken이 유효
- 갱신에 성공하여 200 상태 코드가 오면
- accessToekn 갱신하고 진행 중이던 요청 이어서 진행
- 410 에러
- refreshToken이 만료되었기에 로그아웃
- 409 에러
-요청 처리 interceptors
instance.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Access=${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
- accessToken을 header에 담고 요청을 보냄
-전체 코드
import axios from 'axios';
let accessToken: string;
export const instance = axios.create({
withCredentials: true,
});
export async function reissuanceJwt() {
const res = await axios.post(`/members/refresh`, null, { withCredentials: true });
return res;
}
// 토큰을 함께보내는 privateApi에 interceptor를 적용합니다
instance.interceptors.response.use(
// 200번대 응답이 올때 처리
(response) => {
return response;
},
// 200번대 응답이 아닐 경우 처리
async (error) => {
const {
config,
response: { status },
} = error;
//토큰이 만료되을 때
if (status === 409) {
const originRequest = config;
const response = await reissuanceJwt();
//리프레시 토큰 요청이 성공할 때
if (response.status === 200) {
const newAccessToken = response.data.accessToken;
accessToken = newAccessToken;
//진행중이던 요청 이어서하기
originRequest.headers.Authorization = `Access=${newAccessToken}`;
return axios(originRequest);
//리프레시 토큰 요청이 실패할때(리프레시 토큰도 만료되었을때 = 재로그인 안내)
}
} else if (status === 410) {
sessionStorage.removeItem('userInfo');
window.location.replace('/login');
}
return Promise.reject(error);
}
);
instance.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Access=${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
최종 결과
- 409 에러가 발생
- /members/refresh로 다시 갱신
- refreshToken이 만료가 안됐으면 성공하고 200번 상태 코드가 옴
- 진행 중이던 처리 계속 진행
반응형
'UpLog 릴리즈노트 프로젝트' 카테고리의 다른 글
React Query Loading 중앙화 (0) | 2024.02.17 |
---|---|
로그인 이후의 관리 방식 (0) | 2024.02.17 |
Vite에서 Jest를 사용하지 않고 Cypress만 쓰기로 한 이유 (0) | 2024.02.13 |
MSW2 사용 중 Warning: captured a request without a matching request handler (0) | 2024.02.05 |
UpLog 프론트엔드 CI/CD4(URL 카톡 공유, AWS 과금 알람, 적용 후기) (0) | 2023.09.04 |
Comments