(※ 이 내용은 아직 정확하게 구현되는지 검증되지 않았습니다! 백엔드와 소통없이 프론트에서 독단적으로 작성한 것이니 참고만 해주세요!!!)
로그인을 구현할 때,
1. 단순히 백엔드한테 구글로부터 받은 인가코드를 보내면
2. 받은 토큰을 localStorage에 저장하는 방식으로 구현했었다.
하지만 이 방식은 보안을 고려했을 때 좋은 방법이 아니다. localStorage에 토큰을 저장하는 것은 권장되는 방법이 아니다.
따라서 아래와 같은 방법으로 수정하려 한다.
- 로그인을 하면 서버는 클라이언트한테 Access Token과 Refresh Token을 받음
- Access Token은 전역 변수로 관리(Zustand 활용), RefreshToken은 httpOnly 쿠키로 서버로 부터 받음(이 때 서버와 클라이언트가 도메인이 같다면, 따로 조치 필요x)
- 클라이언트가 새로고침이나 재렌더링이 될 때마다 Access Token의 유효성을 확인하는 요청을 보냄
- 클라이언트는 Refresh Token을 서버로 전송
- 서버에서 Refresh Token의 유효성을 검증하고 새로운 Access Token을 클라이언트로 전송
- 로그아웃 시 클라이언트는 Token을 제거
Access Tokend을 zustand를 통한 전역 변수로 관리하게 되면, 새로고침이나 재렌더링이 되면 변수에 저장되있던 Access Token이 사라지게 된다.
-> 따라서 최상위 컴포넌트인 App.tsx에서 새로고침(재렌더링)이 될 때마다 토큰 유효성 검증을 보내는 코드를 작성해야한다.
useStore.ts
import { create } from 'zustand';
export interface State {
accessToken: string;
setAccessToken: (token: string) => void;
logOut: () => void;
}
const useStore = create<State>((set) => ({
// access Token
accessToken: '',
// access Token 저장
setAccessToken: (token: string) => set({ accessToken: token }),
// 로그아웃
logOut: () => set({ accessToken: '' }),
}));
export default useStore;
zustand를 활용하여 access Token을 전역변수로 관리하기 위하여 위와 같이 코드를 작성했다.
setAccessToken을 통하여 accessToken을 저장하고, logOut이 되면 accessToken을 다시 빈 문자열로 바꾸어준다.
App.tsx
import Router from './Router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import useStore from './store/useStore';
import { useEffect, useState } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios';
import Loading from './pages/Loading';
const queryClient = new QueryClient();
function App() {
// refresh Token을 주고 받기 위한 설정
axios.defaults.withCredentials = true;
const { logOut, setAccessToken } = useStore();
// 토큰 유효성 검증하는 동안 로딩 컴포넌트를 띄우기 위한 상태
const [isLoading, setIsLoading] = useState(true);
// 토큰 유효성 검증
useEffect(() => {
const validateAccessToken = async () => {
try {
// httpOnly 쿠키에 있던 refresh Token이 서버로 전송
// 그리고 응답으로 새로운 Access Token을 받는다
const response = await axios.get('endpoint');
// 새로 받아온 AccessToken 저장
const newAccessToken = response.data.accessToken;
setAccessToken(newAccessToken);
} catch (error) {
// 유효성 검증 실패 시 로그아웃
logOut();
} finally {
// 로딩 컴포넌트 끄기
setIsLoading(false);
}
};
validateAccessToken();
}, [setAccessToken, logOut]);
// 로딩 중이면 로딩 컴포넌트 표시
if (isLoading) {
return <Loading />;
}
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<Router />
</ThemeProvider>
</QueryClientProvider>
);
}
export default App;
axios.defaults.withCredentials = true 구문을 추가해놓으면 HttpOnly 쿠키에 있는 refresh Token이 자동으로 서버가 전송이 된다고 해서 위 구문을 추가했다.(실제로 되는지는 테스트해야 겠지만...)
그리고 refresh Token을 전송하면
1. refresh Token 유효성 검증에 성공했을 시
-> 서버로 부터 새로운 access Token을 받고 zustand 전역 변수로 저장한다.
2. refresh Token 유효성 검증에 실패했을 시
-> 로그아웃한다.
그리고 유효성 검증을 하는 동안 로딩화면을 띄어놓는다.
GoogleLogin.tsx
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Loading from './Loading';
import useStore from '../store/useStore';
const GoogleLogin = () => {
const navigate = useNavigate();
const setAccessToken = useStore((state) => state.setAccessToken);
// 회원가입된 유저일 때: 게시판 페이지로 이동
const handleBoard = () => {
navigate('/board');
window.location.reload();
};
// 회원가입을 해야하는 유저일 때: 회원가입 페이지로 이동
const handleRegister = () => {
navigate('/register/1');
window.location.reload();
};
// 현재 url에서 code 추출
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
// code 백엔드로 보내기 토큰 받기
const loginMutation = useMutation({
mutationFn: async (code: string) => {
// 명세서보고 url 수정할 것
const response = await axios.post('url', { code });
return response.data;
},
onSuccess: (res) => {
// 백엔드와 소통해서 res.data.accessToken이 맞는지 확인 할 것
// access Token을 zustand 라이브러리를 통하여 전역 관리
setAccessToken(res.data.accessToken);
// 회원가입된 유저이냐의 여부에 따라 이동하는 페이지 결정(백엔드와 소통할 것)
res.data.isExistingMember ? handleBoard() : handleRegister();
},
onError: (error) => {
console.log(error);
},
});
useEffect(() => {
if (code) {
loginMutation.mutate(code);
} else {
console.log('로그인 재시도하세요.');
}
}, [code]);
return <Loading />;
};
export default GoogleLogin;
원래는 Access Token을 localStorage에 저장했었는데, setAccessToken 함수를 활용하여 받은 Access Token을 Zustand 전역 변수로 저장했다(refresh Token은 따로 건들 필요가 없어보인다.)
일단 위와 같이 코드를 수정했다. 잘 되는지는 실제로 백엔드와 협업해서 작동해봐야 확인할 수 있다(잘 됐으면 좋겠다 ㅠ)
그리고 잘 되면 네이버 로그인도 추가해보려한다. 그리고 로그인 여부에 따라 글쓰기, 수정, 삭제 등 버튼의 표시 여부도 바꾸는 것도 코딩을 해야한다. 계속 해보자!
'Project > PROlog' 카테고리의 다른 글
[PROlog] Material UI 사용하기(+Tailwind CSS 다크모드 적용) (1) | 2024.03.29 |
---|---|
[PROlog] 인트로 페이지 변경 + Quill editor 도입 (0) | 2024.03.02 |
[PROlog] CRUD 게시판 만들기 프로젝트 시작 (0) | 2024.02.29 |