📝 배경
- 본 프로젝트 (삐삐:Best Interior)에서는 서버가 클라이언트를 인증하는 방식 중 하나인 JWT를 이용하여 로그인 기능을 구현하기로 함
- 또한, 토큰은 로컬스토리지에 저장하는 방식을 택했으며, 이렇게 저장된 토큰은 매 요청 헤더에 담아 적절한 인증 과정을 거친 사용자인지 판단하는 인가(Authorization)를 구현함
- 이를 위해, axios 통신시 interceptors 를 이용하여 헤더에 토큰 정보를 담아 보내는 token.js라는 컴포넌트를 만들었으며, token 컴포넌트를 활용하여 useAxios라는 커스텀훅을 만들어서 통신로직을 추상화하고 재사용성 및 코드 가독성을 향상시키고자 했음
🚨 문제 상황 - (1). api 요청시 헤더에 엑세스토큰을 담아 보냇으나, 403에러 발생
- 로그인 성공 시 로컬스토리지에 토큰(access,refresh) 이 제대로 저장되었고, 해당 토큰을 헤더에 담아 보내는데 403 에러 발생.
- 여기서 403에러는 유효한 토큰이 아닐 시 반환하는 에러코드임
🔍️ 원인 파악
- 원인 파악을 위해 포스트맨에 토큰값을 넣어 요청한 결과 제대로 응답이 오는것을 확인하고, 프론트쪽 코드 문제라는것을 확인함
- 보통 JWT나 Oauth 토큰 인증 타입은 Bearer token을 사용하는점 그리고, 요청 헤더의 필드값으로 Authorization을 사용해야 한다는점을 간과하였음
// 당시 작성 코드
// 토큰 정보를 헤더에 추가하는 함수
const addTokenToHeaders = (config) => {
const refreshToken = localStorage.getItem("refreshToken");
const accessToken = localStorage.getItem("accessToken");
if (refreshToken) {
config.headers["RefreshToken"] = refreshToken;
}
if (accessToken) {
config.headers["AccessToken"] = accessToken; // => Authorization 라는 필드명이 아닌, AccessToken이라는 필드명으로 토큰을 보낸것
}
config.headers["Content-Type"] = "application/json";
config.headers["ngrok-skip-browser-warning"] = "69420";
config.withCredentials = true;
- 즉, 해당 문제의 원인은, 요청 헤더의 필드명 Authorization값으로 적절한 토큰의 타입을 명시를 하지 않아서, 서버쪽에서 해당 토큰을 유효한 토큰이라고 인식하지 못했었던 문제였음
🔨 해결 방안
- 요청 헤더에 적절한 필드명(Authorization)의 필드값으로 access토큰의 값을 넣어주었음
// 토큰 정보를 헤더에 추가하는 함수
const addTokenToHeaders = (config) => {
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`; // 필드명 수정, 필드값에 토큰 인증 타입 지정
}
config.headers["Content-Type"] = "application/json";
config.headers["ngrok-skip-browser-warning"] = "69420";
config.withCredentials = true;
return config;
};
🚨 문제 상황 - (2). 토큰 만료시 재발급 로직 구현
- 엑세스토큰 만료시 재발급하는 로직 구현중 발생한 에러
- 엑세스토큰 만료(유효하지 않은 토큰)시 서버에서 403에러를 반환하고, 해당 에러 발생시 클라이언트에서 리프래시토큰을 보내면, 서버에서 유효한 리프래시토큰임을 확인한 경우 새로운 엑세스토큰과 리프래시토큰을 응답 헤더로 보내도록 이야기가 된 상황이었음
- 사실 이부분은 Fe,Be 모두 처음 구현하려는 기능이다 보니, 각자 공부하고, 큰틀에서만 이야기가 된채로 작업을 하여 오류가 발생할수 밖에 없었던것 같음
- 또한, 해당 파트는 담당자분이 바쁘셔서 제가 중간에 넘겨 받게된 파트였기 때문에, BE담당자분과 디테일한 부분까지 얘기해가며 해결할 수 있었음
🔍️ 원인 파악
토큰 재발급 로직과 관련하여 다음과같은 문제점을 고려하여 새롭게 코드를 짜봄
- 토큰 만료시, 새로운 요청으로 보내는 헤더에 엑세스토큰과 리프래쉬토큰의 적절하지 않았던 필드명
- 토큰 만료시, 리프래시 토큰을 인증하는 별도의 api가 없고, 해당 요청보낸던 url, method, data(body)을 그대로 활용해야함
- 토큰 재발급시 다시 엑세스 토큰 헤더에 담아 같은 요청을 다시 보내야함.
🔨 해결 방안
1. 토큰 만료시, 새로운 요청으로 보내는 헤더에 적절한 필드명과 필드값 적용
- 엑세스토큰은 Authorization 필드값으로, 리프래시토큰은 Authorization-refresh 필드값으로 넣어줌
// 토큰 갱신 및 재시도 함수 (403에러 반환시 실행함수)
const handleTokenRefresh = async (error) => {
// 로컬스토리지에 저장되어있는 엑세스토큰과 리프레시토큰 추출
const accessToken = localStorage.getItem("accessToken");
const refreshToken = localStorage.getItem("refreshToken");
if (refreshToken) {
try {
// 요청보낸 메서드와 url, data(바디값) 저장
const { method, url, data } = error.config;
// 요청보낸 메서드, url, 바디값 및
// 추가로 헤더에 엑세스토큰과 리프레시 토큰을 전송하도록 config설정
const configParams = {
method,
url: `${process.env.REACT_APP_API_URL}${url}`,
headers: {
"Authorization-refresh": `Bearer ${refreshToken}`,
Authorization: `Bearer ${accessToken}`,
},
data,
};
2~3. 토큰 만료시, 리프래시 토큰을 인증하는 별도의 api가 없고, 해당 요청보낸던 url, method, data(body)을 그대로 활용하여 다시 요청을 보냄
- 위의 1과정을 통해 유효한 리프래시 토큰을 받을경우, 새로운 엑세스토큰과 리프래시토큰이 응답의 헤더로 받음
- 새로운 토큰을 로컬스토리지에 저장한 뒤, 새로운 토큰을 사용하여, 이전(만료된 토큰을 통해 보냈던 요청) 요청을 그대로 서버로 전송
// 토큰 갱신 및 재시도 함수 (403에러 반환시 실행함수)
const handleTokenRefresh = async (error) => {
~~~~~~
// 설정한 config로 다시 서버로 요청을 보냄
const response = await axios(configParams);
// 성공적으로 응답시 새로운 엑세스토큰과 리프레시토큰을 로컬스토리지에 저장
const newAccessToken = response.headers.authorization;
const newRefreshToken = response.headers["authorization-refresh"];
localStorage.setItem("accessToken", newAccessToken);
localStorage.setItem("refreshToken", newRefreshToken);
// 헤더에 새로운 엑세스토큰을 포함하여 서버로 재요청
error.config.headers["Authorization"] = `Bearer ${newAccessToken}`;
return api.request(error.config);
} catch (err) {
// 응답없는경우 로그아웃
window.dispatchEvent(new Event("logoutEvent"));
console.log(err);
}
} else {
window.dispatchEvent(new Event("logoutEvent"));
console.log("리프레시토큰 유효하지 않습니다.");
}
};
🎉 JWT 토큰 인가 및 재발급 관련 코드 전체
import axios from "axios";
// Axios 인스턴스 생성
const api = axios.create({ baseURL: process.env.REACT_APP_API_URL });
// 요청에 토큰을 추가하는 함수
const addTokenToHeaders = (config) => {
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
config.headers["Content-Type"] = "application/json";
config.headers["ngrok-skip-browser-warning"] = "69420";
config.withCredentials = true;
return config;
};
// 토큰 갱신 및 재시도 함수 (403에러 반환시 실행함수)
const handleTokenRefresh = async (error) => {
// 로컬스토리지에 저장되어있는 엑세스토큰과 리프레시토큰 추출
const accessToken = localStorage.getItem("accessToken");
const refreshToken = localStorage.getItem("refreshToken");
if (refreshToken) {
try {
// 요청보낸 메서드와 url, data(바디값) 저장
const { method, url, data } = error.config;
// 요청보낸 메서드, url, 바디값 및
// 추가로 헤더에 엑세스토큰과 리프레시 토큰을 전송하도록 config설정
const configParams = {
method,
url: `${process.env.REACT_APP_API_URL}${url}`,
headers: {
"Authorization-refresh": `Bearer ${refreshToken}`,
Authorization: `Bearer ${accessToken}`,
},
data,
};
// 설정한 config로 다시 서버로 요청을 보냄
const response = await axios(configParams);
// 성공적으로 응답시 새로운 엑세스토큰과 리프레시토큰을 로컬스토리지에 저장
const newAccessToken = response.headers.authorization;
const newRefreshToken = response.headers["authorization-refresh"];
localStorage.setItem("accessToken", newAccessToken);
localStorage.setItem("refreshToken", newRefreshToken);
// 헤더에 새로운 엑세스토큰을 포함하여 서버로 재요청
error.config.headers["Authorization"] = `Bearer ${newAccessToken}`;
return api.request(error.config);
} catch (err) {
// 응답없는경우 로그아웃
window.dispatchEvent(new Event("logoutEvent"));
console.log(err);
}
} else {
window.dispatchEvent(new Event("logoutEvent"));
console.log("리프레시토큰이 유효하지 않습니다.");
}
};
// 요청 인터셉터 설정
api.interceptors.request.use(addTokenToHeaders);
// 응답 인터셉터 설정
api.interceptors.response.use(
async function (response) {
return response;
},
async function (error) {
if (error.response && error.response.status === 403) {
// 엑세스 토큰 만료 시 토큰 갱신 및 재시도
return handleTokenRefresh(error);
}
return Promise.reject(error);
}
);
export default api;
'Project > 삐삐(BIBI: Best Interior)' 카테고리의 다른 글
[최적화] Pagenation 구현, 정적리소스 서버 구축 (0) | 2023.10.05 |
---|---|
댓글, 답글 기능 구현 중 발생한 에러 (0) | 2023.09.19 |
[AWS S3] 클라이언트 배포 시 404 에러 (0) | 2023.09.17 |