일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 프로그래밍 언어론
- websocket
- 컴퓨터 네트워크
- useState
- 자바스크립트
- 리액트 훅
- 디자인 패턴
- react firebase
- 데이터모델링과마이닝
- 프로그래머스
- JavaScript
- 프로그래머스 완전탐색
- 코딩테스트 고득점 Kit
- 코틀린
- NextJS
- 리액트
- 백준
- 자바 공부
- vanillaJS
- 프로그래머스 자바
- design pattern
- React JS
- codesandbox
- 장고
- react hook
- 자바
- 코딩테스트 고득점 Kit 완전탐색
- Java
- react
- useEffect
- Today
- Total
기록하는 개발자
[React] axios interceptor를 이용한 token refresh 본문
현재 프로젝트에서는 access_token과 refresh_token을 받아 localstorage에 저장하여 사용자 인증 정보를 사용하고 있다.
두 토큰이 각각 만료되었을 때 처리 해야할 로직이 다르다.
access_token의 만료
프론트 | 백 |
access_token이 만료 된 채로 서버에 api 요청 → | |
← 서버에서 401 error 와 함께 message 로 “expired” 전송 | |
기존의 access_token, refresh_token을 통해 토큰 재발급 요청 → | |
← 토큰 재발급 | |
토큰 저장 후 토큰 만료로 수행하지 못했던 api 요청 → | |
← response |
refresh_token의 만료
프론트 | 백 |
refresh_token이 만료 된 채로 서버에 api 요청 → | |
← 서버에서 401 error와 함께 message로 “만료된 refreshToken입니다.” 전송 | |
Localstorage.clear(token 삭제)후 login page로 navigate하여 재로그인 유도 |
TokenRefresher.tsx 전체 코드
import { useEffect } from "react";
import axios from "axios";
import { config } from "../static/config";
import { useNavigate } from "react-router-dom";
export default function TokenRefresher() {
const navigate = useNavigate();
useEffect(() => {
const refreshAPI = axios.create({
baseURL: `${config.api}`,
headers: { "Content-type": "application/json" },
});
const interceptor = axios.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
const originalConfig = error.config;
const msg = error.response.data.message;
const status = error.response.status;
if (status == 401) {
if (msg == "access token expired") {
await axios({
url: `${config.api}/user/reissue`,
method: "Post",
headers: {
accesstoken: localStorage.getItem("token"),
refreshToken: localStorage.getItem("refreshToken"),
},
})
.then((res) => {
localStorage.setItem("token", res.data.accessToken);
originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;
return refreshAPI(originalConfig);
})
.then((res) => {
window.location.reload();
});
}
else if (msg == "refresh token expired") {
localStorage.clear();
navigate("/login");
window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
}
else if (msg == "mail token expired") {
window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
}
}
else if (status == 400 || status == 404 || status == 409) {
window.alert(msg);
}
return Promise.reject(error);
}
);
return () => {
axios.interceptors.response.eject(interceptor);
};
}, []);
return <></>;
}
axios instance 생성
const refreshAPI = axios.create({
baseURL: `${config.api}`,
headers: { "Content-type": "application/json" },
});
axios.create 를 통해 axios instance를 생성한다.
axios interceptor
const interceptor = axios.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
const originalConfig = error.config;
const msg = error.response.data.message;
const status = error.response.status;
if (status == 401) {
if (msg == "access token expired") {
await axios({
url: `${config.api}/user/reissue`,
method: "Post",
headers: {
accesstoken: localStorage.getItem("token"),
refreshToken: localStorage.getItem("refreshToken"),
},
})
.then((res) => {
localStorage.setItem("token", res.data.accessToken);
originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;
return refreshAPI(originalConfig);
})
.then((res) => {
window.location.reload();
});
}
else if (msg == "refresh token expired") {
localStorage.clear();
navigate("/login");
window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
}
else if (msg == "mail token expired") {
window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
}
}
else if (status == 400 || status == 404 || status == 409) {
window.alert(msg);
}
return Promise.reject(error);
}
);
interceptor 내부 상단의 함수
- 토큰 만료와 상관없이 api 요청에 성공하여 Reponse가 제대로 오는 경우이다.
const interceptor = axios.interceptors.response.use(
//response가 정상적으로 오는 경우
function (response) {
return response;
},
interceptor 내부 하단의 함수
- api 요청 후 error가 발생하는 경우이다.
async function (error) {
const originalConfig = error.config;
const msg = error.response.data.message;
const status = error.response.status;
if (status == 401) {
if (msg == "access token expired") {
await axios({
url: `${config.api}/user/reissue`,
method: "Post",
headers: {
accesstoken: localStorage.getItem("token"),
refreshToken: localStorage.getItem("refreshToken"),
},
})
.then((res) => {
localStorage.setItem("token", res.data.accessToken);
originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;
return refreshAPI(originalConfig);
})
.then((res) => {
window.location.reload();
});
}
else if (msg == "refresh token expired") {
localStorage.clear();
navigate("/login");
window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
}
else if (msg == "mail token expired") {
window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
}
}
else if (status == 400 || status == 404 || status == 409) {
window.alert(msg);
}
return Promise.reject(error);
}
);
변수 선언
const originalConfig = error.config;
const msg = error.response.data.message;
const status = error.response.status;
originalConfig : 기존에 수행하려 했던 작업
msg : 백에서 error response로 보내준 message
status : 현재 발생한 에러 코드
access_token 재발급
if (status == 401) {
if (msg == "access token expired") {
await axios({
url: `${config.api}/user/reissue`,
method: "Post",
headers: {
accesstoken: localStorage.getItem("token"),
refreshToken: localStorage.getItem("refreshToken"),
},
})
.then((res) => {
localStorage.setItem("token", res.data.accessToken);
originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;
return refreshAPI(originalConfig);
})
.then((res) => {
window.location.reload();
});
}
}
);
error의 status가 401이고 msg가 "access token expired" 이면 access_token이 만료됐다고 간주하여 토큰 재발급을 요청한다. 요청 응답이 정상적으로 오는 경우, localstorage의 access_token을 수정한다.
기존에 수행하려 했던 originalConfig의 header에는 이미 만료된 access_token이 들어있다. 따라서, 해당 header의 accessToken을 새로 받은 token으로 수정한다.
axios.create로 선언한 인스턴스를 통해 originalConfig를 요청한다. originalConfig가 정상적으로 수행되었다면 화면이 새로고침 된다.
refresh_token 재발급과 예외 처리
else if (msg == "refresh token expired") {
localStorage.clear();
navigate("/login");
window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
}
else if (msg == "mail token expired") {
window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
}
}
else if (status == 400 || status == 404 || status == 409) {
window.alert(msg);
}
error의 status가 401이고 msg가 "refresh token expired" 이면 refresh_token이 만료됐다고 간주한다.
이에 localStorage를 모두 비우고 login 화면으로 navigate하여 재로그인을 유도한다.
추가적으로 비밀번호 변경 시 이메일로 url에 token이 있는 비밀번호 변경 link를 보내주는데, 해당 token의 유효 시간은 10분이다. 10분이 지난 뒤 비밀번호 변경을 요청하면 401 에러가 발생하는데, 해당 에러의 핸들링을 위해 msg가 "mail token expired" 인 경우를 예외처리 하였다.
이외 400, 404, 409의 에러가 발생하는 경우, 서버에서 보내주는 msg를 경고창으로 띄워준다.
AppRouter.tsx 전체 코드
import 생략
const AppRouter = () => {
return (
<>
<BrowserRouter>
<TokenRefresher />
<Navigation />
<Routes>...</Routes>
</BrowserRouter>
</>
);
};
export default AppRouter;
TokenRefresher 는 Navigation과 마찬가지로 Router 바깥에서 작동하도록 Routes 태그 바깥에 둔다.
'Web > React' 카테고리의 다른 글
[React] react-select를 사용해보자 (1) | 2023.04.07 |
---|---|
[React] React+typescript에서 chartJs의 update() 사용법 (0) | 2023.04.07 |
[React] 그래서 Next.js를 왜 써야하는데? (0) | 2022.12.19 |
[React] Babel과 Webpack (0) | 2022.12.08 |
[React] React hook useState를 사용해 modal창 만들기 (0) | 2022.10.17 |