codestates / codestates/sagaksagak-client
[Error] : #208 / 로그인 유지 로직 관련 에러
- Dominant language
- JavaScript
- Stars
- 4
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
> 해결된 에러라면 라벨에 'Complete' 를 달아주세요.
> 미해결된 에러라면 라벨을 'In progress' 로 변경해주세요.
### 어떤 에러인가요?
- 리프레시 토큰을 사용하여 유저의 로그인을 유지하기 위한 로직을 구성하였으나 403 에러 페이지로 넘겨서 처리하는 로직에서 치명적인 에러가 발생하였습니다. #208 에러의 세부 내용은 다음과 같았습니다.
*로직 구조*
1. useEffect를 사용하여 컴포넌트가 마운트 될때 쿠키를 동반한 AJAX 요청을 보내 재 로그인 요청
2. 서버의 로직은 클라이언트에서 받은 쿠키가 null이 아니라면 유저의 정보를 클라이언트로 전송
2-1. 만일 null이라면(쿠키가 만료 되거나 로그아웃을 통해 쿠키의 내용이 '빈문자열'로 대체 된경우) 403을 클라이언트로 응답
3. 서버가 200 OK 응답을 내려준다면, 응답의 data에 들어있는 유저정보를 다시 상태에 저장하여 로그인 상태를 유지(재 로그인)
3-1. 서버가 403 응답을 내려준다면 403 에러 페이지로 리디렉션 이후 유저가 재 로그인 진행
*에러 1*
1. 쿠키가 '만료' 된다면 만료 되기 전 받은 쿠키가 그대로 클라이언트의 브라우저상에 저장되어 있음 유저가 로그아웃을 진행한 경우라면 빈문자열로 토큰이 대체된 쿠키가 클라이언트의 브라우저상에 저장되어 있음
2. 클라이언트 상에서 로그인 상태가 사라질 경우중 가장 흔한 경우는 '새로고침'이기 때문에 새로고침 행동에 집중하여 컴포넌트가 마운트 될때 useEffect를 통해 AJAX 요청을 서버로 보냄
2-1. 서버상에선 쿠키가 만료 되었거나 유저가 로그아웃을 진행해 빈문자열로 대체한 경우나 모두 DB 쿼리시 쿼리 결과가 null로 같음
2-2. 따라서 세부 상황과 관계없이 서버단에서 쿼리 결과가 null이면 클라이언트로 403을 내려줌
2-3. 클라이언트는 서버 응답이 403일 경우 403 에러 페이지로 리디렉션 시킴
2-4. 리디렉션 된 403 에러 페이지의 상호작용 버튼을 클릭하면 랜딩페이지로 리디렉션 됨
2-5. 랜딩페이지가 다시 마운트 되었기 때문에 AJAX요청을 보내고 쿠키는 만료 혹은 빈문자열 상태이기 때문에 2-1로 돌아가 무한 루프 상황이 발생함
*에러 2*
1. 한번이라도 로그인을 한 유저라면 유저가 쿠키를 클린업 하지 않는 이상 클라이언트 브라우저상에는 쿠키가 남아 있음
2. 에러 1을 해결하기 위해 랜딩페이지의 AJAX 요청 header에 특정 키-벨류를 보냈으나 해당 키-벨류를 보내는 조건을 랜딩페이지로 잡아 랜딩페이지는 다시 무한 루프에 빠지게 됨
### 에러 메시지
```bash
$ 터미널의 에러 코드를 여기 넣어주세요.
```
### 에러 핸들링 방법
- 클라이언트의 코드 상에서 쿠키를 뽑아낼 수 있다는 것을 알고 쿠키를 조건문의 조건으로 넣어 상황을 구분함(document.cookie)
- 클라이언트 입장에서 쿠키는 3개의 상태가 존재함 (만료된 쿠키, 로그아웃으로 인해 빈 문자열로 대체된 쿠키, 쿠키가 없음)
- 유저가 겪는 상황은 다음과 같이 정리 됨
* 한번도 회원가입이나 로그인을 진행하지 않고 비회원 기능을 이용하는 경우
* 한번이라도 로그인을 하여 서비스를 이용하고 로그아웃을 한 경우
* 새로고침을 한 경우
* 로그인을 너무 오래 유지하여 리프레시 토큰이 만료된 경우
- 이에 따라 클라이언트에서 쿠키를 추출하여 쿠키가 존재하지 않거나 빈 문자열로 대체된 경우 useEffect 내부의 해당 로직이 돌아가지 않도록 방지함
- 토큰이 만료된 경우에는 서버에 로그아웃 요청을 보내고 클라이언트 단에서도 로그아웃을 진행함 dispatch(logOut())
- 토큰이 만료된 경우 로그아웃을 진행하면 서버단에서 빈문자열로 대체된 쿠키를 보내게 됨
- 해당 쿠키는 조건문에 걸려 로그인 로직이 돌아가지 않도록 함
```js
//클라이언트 단에서 쿠키를 추출하기 위한 유틸 함수
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
};
export default getCookie;
-----------------------------------------------------------------------------------------------------------------------------
const handleRefreshLogIn = () => {
//refreshToken 이라는 이름의 쿠키가 존재하지 않거나 빈 문자열인 경우 함수를 실행하지 않고 바로 리턴
if (!getCookie("refreshToken")) return;
axios
.get(`${process.env.REACT_APP_SERVER_DOMAIN}/user/token`, {
headers: {
relogin: true,
},
withCredentials: true,
})
.then((res) => {
const { accessToken, username, userId, email, category, subId } =
res.data;
if (subId)
dispatch(
logIn(email, userId, username, accessToken, category, subId)
);
dispatch(logIn(email, userId, username, accessToken, category));
})
.catch((err) => {
if (err.response) {
if (err.response.status === 403) {
//403 응답을 받으면 서버에 로그아웃 요청을 보냄
axios
.get(`${process.env.REACT_APP_SERVER_DOMAIN}/user/logout`, {
headers: { authorization: `bearer ${user.accessToken}` },
withCredentials: true,
})
.then(() => {
//로그아웃 응답을 받으면 클라이언트 로그아웃 진행
dispatch(logOut());
})
.catch((err) => {
if (err.response) {
console.log(err.response);
} else if (err.request) {
console.log(err.request);
} else {
console.log("Error :", err.message);
}
console.log(err.config);
});
//서버, 클라이언트 로그아웃 진행 후 403 에러 페이지로 리디렉션
history.push("/unauthorized");
}
console.log(err.response);
} else if (err.request) {
console.log(err.request);
} else {
console.log("Error :", err.message);
}
console.log(err.config);
});
};
```
### 에러 핸들링을 위해 참고한 레퍼런스 링크
[https://stackoverflow.com/questions/10730362/get-cookie-by-name]()
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.