이번 프로젝트에서 로그인 부분을 담당하게 되었다.
카카오 로그인과 이메일 로그인을 구현해야 하는데, 이미 백엔드 개발자분께서 만들어 놓으신 api가 있어서 쉽게 할 수 있을 것 같았지만...
중간에 내가 알고있는 방식대로 되지 않아 새로운 방식을 찾아서 하느라 고생을 좀 했다 😂
그 과정에서 배운 내용들을 정리해 보고자 한다.
먼저 카카오 로그인을 구현한 내용을 보기 앞서 카카오 로그인의 과정을 살펴보면
이 그림(출저: https://data-jj.tistory.com/53)과 일치한다.
내가 프론트에서 다룬 부분은 1번, 2번, 3번, 8번
인데, 이전에 React를 사용한 프로젝트에서 했던 방식으로 하려했고, 그 계획은 이랬다.
첫째로 kakao의 api를 통해 인가코드를 받아온다.
둘째로 받아온 인가코드를 api를 통해 서버에게 넘긴다.
셋째로 api는 리턴값으로 JWT
를 전달해 주는데 이를 decode하여 user 정보로 활용하고 localStorage
에 저장하여 로그인 정보를 유지하려고 했다.
이때 세번째 과정에서 문제가 발생했다 👿
nextjs
를 사용하면서 pre-rendering을 하기위해 Server-side에서 데이터를 fetching 하는데, 이때 user의 데이터가 필요하여 JWT
를 decode해서 가져와야 한다. 하지만 Server-side에서 localStorage를 접근하지 못해 에러가 발생한 것이다.
문제를 해결하기 위해 구글링을 하던중, 보통 JWT
를 cookie
에 저장하여 관리하는 것을 알 수 있었다. 그 과정에서 localStorage에 저장했을때 발생하는 보안적인 이슈도 알 수 있었다. 물론 지금 프로젝트에서는 Access Token
과 Refresh Token
을 카카오에서 알아서 관리하고 갱신시켜 주기 때문에 상관 없지만, 나중에 직접 Access Token
과 Refresh Token
을 관리하게 될 수 있으므로 배워두면 좋을 것 같다
localStorage 저장 방식
localStorage에 저장하게 되면은 XSS공격에 의해 JWT를 탈취당할 수 있다!
https://academind.com/tutorials/localstorage-vs-cookies-xss
cookie 저장 방식
cookie에 저장하는 방식 또한 XSS, CSRF 공격에서 자유로울 수 없지만, cookie의 SameSite, httpOnly, Secure 등의 속성으로 보안 이슈를 어느정도 해결할 수 있다. 그리고JWT
를 다른 도메인에서 사용할 일이 없고, api 서버도 같은 도메인을 사용하면Access Token
과Refresh Token
모두cookie
에 담아 관리할 수 있다. 이에 관련된 더 자세한 내용은 이 게시글, 게시글들을 확인해보면 된다!
cookie
를 설정하는 과정을 클라이언트 코드에서 숨기고 pre-rendered 페이지에 로그인 정보를 적용하기 위해 nextjs
의 api-route 기능을 사용하였다.
첫째로 kakao의 api를 통해 인가코드를 받아온다.
둘째로 받아온 인가코드를 /api/loginKakao
경로의 api를 호출하여 next 서버로 넘긴다.
셋째로 next 서버에서 우리 api를 호출하여 인가코드를 우리 서버로 전달한다.
넷째로 리턴 받은 JWT
를 set-header
를 통해 cookie
에 저장한다. error
를 리턴 받은 경우 error
메세지를 클라이언트에 전달한다.
// 1.kakao에서 인증코드 받아오기
const login = () => {
window.Kakao.Auth.login({
throughTalk: false,
success: function (authObj) {
onSuccess(authObj);
},
fail: function (err) {
console.log('카카오에서 인증코드 받아오는 과정에서 오류발생', err);
alert('카카오에서 인증코드 받아오는 과정에서 오류발생');
},
});
};
const onSuccess = async (res) => {
// 2.인증코드를 받아오는데 성공하면 next서버로 인증코드를 보냄
await axios
.post('/api/loginKakao', {
token_type: res.token_type,
access_token: res.access_token,
expires_in: res.expires_in,
refresh_token: res.refresh_token,
refresh_token_expires_in: res.refresh_token_expires_in,
})
.catch((err) => {
console.log(err);
alert('서버에서 오류발생');
});
//로그인 성공 후 이전 홈으로 이동
router.push('/');
};
import axios from 'axios';
import { LOGIN_WITH_KAKAO } from '/gql/_mutation';
export default async function loginKakao(req, res) {
const {
body: {
token_type,
access_token,
expires_in,
refresh_token,
refresh_token_expires_in,
},
} = req;
//우리 서버로 토큰을 넘기는 과정
const { data } = await axios
.post(process.env.NEXT_PUBLIC_API_SERVER, {
query: LOGIN_WITH_KAKAO,
variables: {
access_token: access_token,
expires_in: expires_in,
refresh_token: refresh_token,
refresh_token_expires_in: refresh_token_expires_in,
token_type: token_type,
},
})
.catch((err) => {
res.status(500);
});
//쿠키에 저장
res.setHeader(
'Set-Cookie',
`celebstock=${data.data.loginKakao.jwt}; path=/; domain=.celebstock.kr; Max-Age=${refresh_token_expires_in}`,
);
res.status(200).send();
}
cookie
에 저장만 했을 뿐 cookie
에 속성을 제대로 지정해 주지 않았다. httpOnly
, samesite
와 같은 속성은 이후에 좀 더 확인해보고 수정을 해야할 것 같다.