-
[Toy Project 기록하기 7] 로그인 1. jwt로 로그인 구현하기Frontend/Projects 2023. 3. 18. 14:20
앞선 포스팅에서 세션 인증 방식과 토큰 인증 방식의 차이점에 대해 알아보았는데요.
2023.03.09 - [Frontend/Projects] - [Toy Project 기록하기 5] 세션 인증 방식 vs 토큰 인증 방식
[Toy Project 기록하기 5] 세션 인증 방식 vs 토큰 인증 방식
HTTP는 stateless한 특성을 가지고 있어 서버는 클라이언트의 상태를 저장하지 않습니다. 유저가 로그인을 했더라도 다음 통신에서 클라이언트의 상태를 기억하지 못해 매번 다시 로그인을 요청하
jihye-dev.tistory.com
해당 글에서 설명한 jwt를 방식으로 Access, Refresh Token을 이용해 로그인을 구현해 보겠습니다.
기본 로그인 조건
✔️ email의 존재 여부를 확인한다.
✔️ email이 존재할 경우, db에 저장되어 있는 비밀번호와 입력한 비밀번호가 일치하는지 확인한다.
email이 존재하지 않거나 비밀번호가 일치하지 않으면 로그인에 실패합니다.
로그인 성공 시 클라이언트에 Refresh Token과 Access Token을 전달합니다.
로그인 로직
코드를 작성하기 앞서 클라이언트와 서버가 이해하기 쉽게 어떤 과정으로 토큰을 주고 받는지 설계해보았습니다. 아래 설계를 바탕으로 로그인을 구현해보겠습니다.
1. 먼저 다른 도메인에 요청을 보낼 때 쿠키 등 인증 정보를 주고받기 위해 axios withCredentials를 true로 설정해야 합니다.
// axios.ts export const axiosInstance = axios.create({ baseURL: SERVER_DOMAIN, withCredentials: true, }); // ...
2. 로그인 axios 요청 보냅니다.
const { mutate: loginMutation, isLoading, isError, error, } = useMutation(loginUser, { // loginUser로 axios 요청 후 성공 시 onSuccess 실행 onSuccess: (data) => { onLoginSuccess(data); // ... }, }); const onValid: SubmitHandler<TLoginForm> = ({ email, password, }: TLoginForm) => { loginMutation({ email, password }); };
export const loginUser = async (data: TLoginForm) => { return axiosInstance.post<TLoginForm>('/user/login', data); };
3. 서버에서 로그인에 성공 시 클라이언트에게 토큰을 보냅니다.
- Refresh Token은 secure httpOnly 쿠키로, Access Token은 JSON payload로 보냅니다.
const getAccessToken = (email: string) => // access token 생성 (15분간 유효) jwt.sign( { email, }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' } ); const getRefreshToken = (email: string) => // refresh token 생성 (180일간 유효) jwt.sign( { email, }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '180d' } ); const refreshAccessTokenOption = { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 180, sameSite: 'none', secure: true, } as CookieOptions; const sendTokens = ( user: HydratedDocument<TUser>, res: Response, message: string ) => { const accessToken = getAccessToken(user.email); const refreshToken = getRefreshToken(user.email); res .cookie('refreshToken', refreshToken, refreshAccessTokenOption) .send({ accessToken, message }); }; export const loginUser = async (req: Request, res: Response) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) { return res.status(400).send({ message: FAILURE.CannotFindUser }); } const matchedPassword = await bcrypt.compare(password, user.password); if (!matchedPassword) { return res.status(400).send({ message: FAILURE.WrongPassword }); } // 로그인 성공 시 sendTokens return sendTokens(user, res, SUCCESS.Login); } catch (error) { return res.status(500).send({ message: DEFAULT_ERROR_MESSAGE }); } }; // ...
4. 로그인 성공 시 Access Token을 로컬 변수에 담고 api 요청 시 헤더 authorization에 넣어 보내줍니다.
export const onLoginSuccess = (response: AxiosResponse) => { const accessToken = response.data.accessToken; axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`; }; // ...
5. Access Token 만료 시 axios의 interceptors를 통해 새로운 Access Token을 받도록 요청합니다.
- axios의 interceptors를 활용해 then 또는 catch로 처리되기 전에 요청이나 응답을 가로챌 수 있습니다.
- axios 요청 오류 시, Access Token이 만료된 것으로 보고 refreshAccessToken으로 새로운 axios 요청을 보냅니다.
// axios.ts // 응답 인터셉터 추가하기 axiosInstance.interceptors.response.use( (response) => { return response; }, async function (error) { if (error.config.retry || error.response.status !== 401) { error.config.retry = false; return Promise.reject(error); } try { const result = await refreshAccessToken(); const newAccessToken = result.data.accessToken; axiosInstance.defaults.headers.Authorization = `Bearer ${newAccessToken}`; error.config.retry = true; return axiosInstance.request(error.config); } catch (error: any) { return Promise.reject(error); } } ); // ...
export const refreshAccessToken = () => { return axios.post<TAccessAuth>( `${SERVER_DOMAIN}/user/refresh-access-token`, {}, { withCredentials: true } ); }; // ...
6. 서버에서 Refresh Token을 통해 새로운 Access Token을 받아와 자동으로 로그인 상태가 유지되도록 합니다.
export const refreshAccessToken = async (req: Request, res: Response) => { try { const token = req.headers.cookie?.split('refreshToken=')[1]; if (!token) { return res.status(401).send({ message: FAILURE.InvalidToken }); } try { const payload = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET); const { email } = payload as TTokenPayload; const user = await User.findOne({ email }); if (!user) { return res.status(400).send({ message: FAILURE.CannotFindUser }); } return sendTokens(user, res, SUCCESS.RefreshAccessToken); } catch (error) { return res.status(401).send({ message: FAILURE.IsLogin }); } } catch (error) { return res.status(500).send({ message: DEFAULT_ERROR_MESSAGE }); } }; // ...
🧗♀️ 제가 진행한 프로젝트가 궁금하다면
🔽 프론트엔드는 이곳에서 확인하실 수 있습니다.
https://github.com/Team-Madstone/doljabee-fe
GitHub - Team-Madstone/doljabee-fe: Climbing Community
Climbing Community. Contribute to Team-Madstone/doljabee-fe development by creating an account on GitHub.
github.com
🔽 백엔드는 이곳에서 확인하실 수 있습니다.
https://github.com/Team-Madstone/doljabee-be
GitHub - Team-Madstone/doljabee-be: Climbing Community
Climbing Community. Contribute to Team-Madstone/doljabee-be development by creating an account on GitHub.
github.com
참고 자료
반응형'Frontend > Projects' 카테고리의 다른 글
[Toy Project 기록하기 9] React-Query로 서버 상태 관리하기 (0) 2023.03.20 [Toy Project 기록하기 8] 로그인 2. 전역 상태 똑똑하게 공유하기 with Context API (0) 2023.03.19 [Toy Project 기록하기 6] 회원가입 & 이메일 인증하기 (0) 2023.03.16 [Toy Project 기록하기 5] 세션 인증 방식 vs 토큰 인증 방식 (0) 2023.03.09 [Toy Project 기록하기 4] 파일 업로드하기 with multer (0) 2023.03.05