서버에 로그인 후 response를 확인해 보니 accessToken과 refreshToken이 반환 되었다
refreshToken은 뭔지 모르겠으니 일단 패스하고, accessToken으로 데이터 요청하자.
const postArticleComment = async ({
id,
content,
}: PostCommentInterface): Promise<ArticleInquiryInterface> => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/articles/${id}/comments`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
body: JSON.stringify({ content }),
}
);
const result = await response.json();
if (response.ok) {
return result;
} else {
throw new Error("댓글 추가 실패");
}
} catch (error) {
throw error;
}
};
PATCH 요청 잘 된다. Access Token이 있으니 이제 나도 서버에 데이터 추가, 수정, 삭제 등 다양한 작업 하면서 좀 더 개발자스러운 프로젝트를 할 수 있겠군😆
게시글 추가 기능도 만들고 나서 확인차 POST 요청을 보냈는데, 결과는 401 에러다.
MDN의 HTTP status에 대해 찾아보니, 유효한 인증 자격 증명이 없기 때문이라고 한다.
나는 분명 Access Token을 발급받아 local storage에서 관리하고 있는데, 왜 자격이 없다는 건지 모르겠다.
JWT 토큰은 보안을 위해 토큰 발급 시 토큰 만료 시간(exp: expiration time)을 생성해 클라이언트에게 전달한다고 한다.
즉, 내가 로그인해서 받은 Access Token이 서버에서 정해준 만료 시간이 지나서 자격 증명이 없다는 것이었다.
그럼 요청을 보내서 401 에러가 날 때마다 다시 로그인을 해야하나? 너무 번거로운데...🤔
JWT 인증 동작 방식을 찾아보니, 이럴 때 사용하라고 서버에서 response로 Refresh Token을 발급해준거였다.
서버에 데이터를 요청할 때 Access Token을 헤더에 함께 보내는데, 서버는 토큰으로 사용자의 자격을 검증한다.
만약 Access Token이 만료되었다면, 클라이언트에게 토큰 만료 응답(401)을 전달한다. 그럼 클라이언트는 Refresh Token을 서버에 전달해 새로운 Access Token과 Refresh Token을 발급 받게 된다. 즉, 다시 로그인 할 필요 없이 로그인을 연장하는 것이다.
아래 코드는 API 요청 시 response로 401 status가 내려오면 서버에 refreshToken을 보내서 새로운 Access Token과 Refresh Token을 발급 받고 API 요청을 다시 시도하는 로직이다.
const retryFetch = async (
url: string,
options: RequestInit
): Promise<Response> => {
const response = await fetch(url, options);
if (response.status === 401) {
// status === 401 : Unauthorized 토큰 갱신
const refreshResponse = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/auth/refresh-token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refreshToken: localStorage.getItem("refreshToken"),
}),
}
);
if (refreshResponse.ok) {
const { accessToken, refreshToken } = await refreshResponse.json();
// localStorage에 갱신된 토큰 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
// requset 재요청
const newOptions = {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`,
},
};
return await fetch(url, newOptions);
} else {
throw new Error("토큰 갱신 실패. 다시 로그인하세요.");
}
}
return response;
};
401 에러로 토큰이 만료되었다는 에러가 내려오지만, Refresh Token을 갱신해 API 요청을 다시 시도하고 정상적으로 완료되었다🙌
마무리하려고 했는데, 한가지 문제가 더 발생했다. 401 에러가 토큰 만료가 아닌 잘못된 토큰을 보낼 때도 발생한다는 것이다. 해결 방법을 찾아보니 클라이언트에서 처리하는 방법과 서버와 클라이언트가 협력하는 방법으로 나뉘었다.
클라이언트에서 처리하는 방법
response의 status만 확인하는 것이 아니라 응답 본문(response.json())의 에러 메시지도 확인하여 적절한 로직을 실행한다.
const retryFetch = async (
url: string,
options: RequestInit
): Promise<Response> => {
const response = await fetch(url, options);
if (response.status === 401) {
// 응답 본문 확인
const errorData = await response.json();
if (errorData.error === "token_expired") {
// 토큰 만료 시 갱신 로직
const refreshResponse = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/auth/refresh-token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refreshToken: localStorage.getItem("refreshToken"),
}),
}
);
if (refreshResponse.ok) {
const { accessToken, refreshToken } = await refreshResponse.json();
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
// 갱신된 토큰으로 요청 재시도
const newOptions = {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`,
},
};
return await fetch(url, newOptions);
} else {
throw new Error("토큰 갱신 실패. 다시 로그인하세요.");
}
} else if (errorData.error === "invalid_token") {
// 잘못된 토큰일 경우: 로그아웃 처리
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
throw new Error("잘못된 토큰입니다. 다시 로그인하세요.");
} else {
throw new Error("인증 에러가 발생했습니다.");
}
}
return response;
};
서버와 협력
서버에서 response를 내려줄 때 토큰 만료와 기타 인증 문제를 명확히 구분해 문서화하고, 상황에 맞는 에러 메시지를 내려준다.
'프로젝트' 카테고리의 다른 글
[Axios] interceptors 적용 - 토큰 재발급 (4) | 2024.12.10 |
---|---|
[Fandom-K] SCSS 추가 및 기초 설정 (0) | 2024.10.26 |
[Fandom-K] 프로젝트 생성 & 초기 설정 (0) | 2024.10.25 |