JWT 를 이용한 로그인 기능 구현 과정
사용한 라이브러리
구현 내용
export const Test = () => {
const cookies = new Cookies();
const {
register,
handleSubmit,
// formState: { errors },
setFocus,
} = useForm<Inputs>({ mode: "onSubmit" });
//focus
useEffect(() => {
setFocus("id");
cookies.get("user_save_id") && setFocus("password");
}, [setFocus]);
//쿠키에 아이디 값이 저장되어 있으면 아이디 저장 체크박스를 true로 설정
const [idChecked, setIdChecked] = useState<boolean>(
cookies.get("user_save_id") ? true : false
);
//로그인 시 아이디 저장 유무 체크
if (idChecked) {
const expires = new Date();
expires.setDate(expires.getDate() + 30);
cookies.set("user_save_id", data.id, { expires });
} else if (!idChecked && cookies.get("user_save_id")) {
cookies.remove("user_save_id");
}
};
const onSubmit: SubmitHandler<Inputs> = async (data) => {
console.log(data);
const res = await CallApi.request(
"/api/v1/users/signin",
"POST",
null,
null,
data
);
//token 쿠키에 저장
cookies.set("accessToken", res.accessToken, {
secure: true,
// httpOnly: true, XSS 공격으로부터 보호
});
cookies.set("refreshToken", res.refreshToken, {
secure: true,
// httpOnly: true,
});
return (
<div id="Login">
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
defaultValue={
cookies.get("user_save_id") && cookies.get("user_save_id")
}
placeholder="아이디"
{...register("id")}
/>
{/* register()의 인자로 들어가는 값이 input의 name이다 */}
{/* {errors && errors.id?.message} */}
<input
type="password"
placeholder="비밀번호"
{...register("password")}
/>
<label>
<input
type={"checkbox"}
name={"idCheck"}
checked={idChecked}
onChange={() => setIdChecked(!idChecked)}
/>
아이디저장
</label>
<input type="submit" />
</form>
</div>
);
};
axios 인터셉터로 토큰 만료기간 체크하기
토큰이 만료되었기 때문에 response 값으로 error를 내려줄텐데,
이 에러를 인터셉트해서 accessToken을 재발급 하고, 새로 발급된 토큰으로 재요청을 실행한다.
그전에 프론트에서 refreshToken이 만료되었는지 체크를 먼저 해야한다.
//Check refreshToken rxpiry
const validateTimeRefreshToken = () => {
const refreshToken = cookies.get("refreshToken");
const decodeRefreshToken: JwtPayload = jwt_decode(refreshToken);
let exp = Number(decodeRefreshToken.exp);
let timestamp = new Date().getTime();
let nowDate: number = Number(
timestamp.toString().slice(0, String(timestamp).length - 3)
);
if (nowDate < exp) {
console.log("RefreshToken is valid");
return true;
} else {
console.log("RefreshToken is invalid");
return false;
}
};
refreshToken이 아직 유효하다면 accessToken을 재발급 받고,
refreshToken이 만료되었다면 쿠키에 저장된 토큰을 모두 삭제한뒤, 로그아웃시킨다.
💡여기서 잠깐, 만약 한 페이지에서 동시에 여러개의 api호출이 이루어진다면 어떻게 될까.
request가 3번 있었다면, error도 3번 발생하므로
토큰 재발급 호출이 requset 개수 만큼 중복되어 실행되는 현상이 생긴다.
기존 refreshToken으로 accessToken 재발급은 단 한 번만 실행되도록 설정해주어야 한다.
아래 자료가 상당히 도움이 되었다.
👀토큰이 만료된 상태에서 request가 여러개 동시에 이루어졌을 때,
토큰 재발급 호출이 requset 개수 만큼 중복되어 실행되는 현상 막기
https://gist.github.com/mkjiau/650013a99c341c9f23ca00ccb213db1c
https://javascript.plainenglish.io/handle-refresh-token-with-axios-1e0f45e9afa
전체 코드
instance.interceptors.response.use(
function (response) {
return response;
},
function (err) {
if (err.config.headers.Authorization !== undefined) {
//Handle expired token
if (
err.response.status === 401 &&
!err.config._retry &&
err.response.data.message === "JWT 토큰이 만료되었습니다."
) {
err.config._retry = true;
console.log(err.response.data.message);
if (validateTimeRefreshToken()) {
return (async () => {
try {
//[api 호출이 여러개여도 accessToken 재발급은 한 번만 실행하도록 설정]
//-> 최초 refreshing_token에 값이 null이므로 refresh_token()실행 O
//-> 그 다음부터는 refreshing_token의 값이 refresh_token()에서 return한 res값이므로 refresh_token()실행 X
refreshing_token = refreshing_token
? refreshing_token
: refresh_token();
const res: any = await refreshing_token;
if (res.data.code === 200) {
console.log("토근 재발급 성공");
cookies.set("accessToken", res.data.data.accessToken, {
secure: true,
});
cookies.set("refreshToken", res.data.data.refreshToken, {
secure: true,
});
err.config.headers.Authorization =
"Bearer " + cookies.get("accessToken");
//Axios Retry
return instance(err.config);
}
} catch (err: any) {
console.log(err.response.data.message);
//refreshToken 에러 스펙 정리 400
if (err.response.data.data.errorCode === "REFRESH_TOKEN_USED") {
console.log("true");
window.location.href = "/signin";
cookies.remove("accessToken");
cookies.remove("refreshToken");
}
}
})();
} else if (!validateTimeRefreshToken()) {
window.location.href = "/signin";
cookies.remove("accessToken");
cookies.remove("refreshToken");
}
}
}
return Promise.reject(err);
}
);
⭐️ 도움이 되었던 자료
https://inpa.tistory.com/entry/TIP-자바스크립트-타임스탬프
https://hydev.tistory.com/3