로그인 기능을 구현하기 전, 도대체 무엇을 알아야 로그인 기능을 구현할 수 있을지 막막했다. 내가 무엇을 모르는지조차 모르는 상태였기 때문에 한숨만 쉬어지던 때가 있었다. 로그인 기능을 처음 구현하는 사람이라면, 누구나 이런 마음이 들 수 있다는 생각에 로그인 문외한이었던 내가 어떤 과정으로 무엇을 공부했는지 공유해 보려 한다!
JWT 를 이용해 로그인을 구현한다는 말을 많이 들어봤지만, 구체적으로 JWT 가 뭐고, 토큰을 어떻게 사용해야할지 모르는 사람들이 많을 것 같다.
먼저 JWT 란 JSON Web Token 의 준말로 클라이언트와 서버가 암호화된 정보를 안전하게 주고 받고 싶을 때 사용하는 특별한 도구이다.
JWT 토큰은 아래와 같이 생겼는데,
여기서 signature 부분에 서버가 중요한 정보를 암호화시켜 클라이언트로 보내주게 된다.
우리가 로그인 기능을 구현하는 이유를 생각해 보자. 이는 각각의 회원을 구분하고, 각각의 회원에게 독립된 데이터를 전달하기 위함이다. 즉 타인은 나의 정보를 보면 안 되고, 나는 나의 정보를 볼 수 있어야 한다. 중앙 집중식 서버는 여러 클라이언트로 오는 요청을 동시에 받는다. 그럼 다수의 클라이언트를 구별할 수 있는 일종의 키가 필요하다. 그리고 그 키는 다른 누군가가 보면 안 되는 중요한 정보이다.
즉, 클라이언트를 구분해 주는 중요한 보안 키의 역할을 JWT 가 훌륭하게 해줄 수 있으므로 우리는 JWT 를 사용하는 것이다.
JWT 의 특징 중 statelss 하다는 것이 있다. 이는 다시 말해 서버가 클라이언트의 인증 상태를 저장하지 않고, JWT 를 요청과 함께 보내면 그때 그때 허용을 해준다는 것이다. 따라서 JWT 는 한 번 만들어지면 제어가 불가능하기 때문에 반드시 토큰의 만료 시간을 지정해 주어야 한다.
만약 JWT 토큰의 만료 시간을 필요 이상으로 길게 주면 어떤 일이 생길까? 토큰이 탈취된 경우 탈취자가 토큰을 악용할 가능성이 커진다는 보안상의 이슈가 생길 수 있다. 그렇다고 만료 시간을 짧게 해버리면 사용자는 계속해서 로그인을 해야 하는 불편함을 겪을 수 있다.
보안상의 이슈도 해결하면서 + 사용자가 계속해서 로그인을 하지 않아도 되게 하려면 어떻게 해주어야 할까?
그에 대한 해답으로 나온 것이 access token 과 refresh token 이다.
Access Token : 사용자를 식별하는 데 사용된다. 유지 시간이 상대적으로 짧은 편.
Refresh Token : 새로운 Access Token 을 발급받는 데 사용된다. 유지 시간이 상대적으로 긴 편.
두 개의 기능을 하는 토큰 두 개를 만들어 이 둘을 함께 사용하면 된다. 무슨 말일까?
먼저 상대적으로 지속 시간이 짧은 Access Token 을 만들어 클라이언트를 식별한다. 그럼 토큰이 탈취되었을 때 토큰이 악용될 가능성이 낮아진다.
그리고 이 토큰을 재발급하는 것을 클라이언트가 하는 대신에 Refresh Token 을 이용해 서버 측에서 자동으로 해준다. 그럼 클라이언트는 Refresh Token 이 만료되었을 때만 재요청을 하면 된다. 이때 Refresh Token 의 지속 시간을 적당히 길게 잡아주면 유지 기간이 길면서도 보안성은 강화된 로그인 기능을 구현할 수 있게 된다.
토큰을 받아오면 다수의 페이지에서 해당 토큰으로 유저 인증을 진행하게 된다. 이럴 때 전역적으로 쉽게 토큰에 접근하기 위해 크게 세 가지 저장소가 쓰인다.
세 가지 모두 장단점이 있지만 한 가지 눈 여겨 볼 점은 local storage 는 스크립트 실행으로 값을 가져올 수 있어 XSS 공격에 취약한 반면 쿠키는 httpOnly 와 secure 속성을 활용해 이를 막을 수 있다는 점이다. 이 셋의 차이에 대해서는 아래 블로그 글의 설명으로 갈음하려 한다.
https://youngman12.tistory.com/15
위의 필수 지식들의 flow 를 바탕으로 하나씩 차근차근 구현해 보도록 하자.
const handleLogin = async () => {
try {
const response = await axios.post(
`${process.env.BASE_URL}api/test/auth/login`,
{
id: 'test',
password: 'testtest',
accessTokenExpiredTime: accessTokenExpiredTime,
refreshTokenExpiredTime: refreshTokenExpiredTime,
},
);
const { accessToken, refreshToken } = response.data;
Cookies.set('accessToken', accessToken, {
expires: accessTokenExpiration,
});
Cookies.set('refreshToken', refreshToken, {
expires: refreshTokenExpiration,
});
router.push('/home');
} catch (e) {
console.log(e);
}
};
이때 한 가지 주의할 점. 쿠키의 값에서부터 토큰 값을 읽어온다고 한다면, 쿠키에서의 토큰 지속 시간을 정해 주어야 한다. 이는 Cookiew.set 의 세 번째 인자로 전달하는 expires 객체를 통해 구현할 수 있다.
useEffect(() => {
if (!refreshToken) {
router.push('auth/login');
}
}, [refreshToken]);
useEffect(() => {
if (!accessToken) {
axios
.post(`${process.env.BASE_URL}/api/test/auth/refresh`, {
refreshToken:
'refresh 토큰 값',
accessTokenExpiredTime: 600,
refreshTokenExpiredTime: 3600,
})
.then((response) => {
console.log(response);
const newAccessToken = response.data.accessToken;
Cookies.set('accessToken', newAccessToken);
router.push('/home');
})
.catch((e) => {
console.log(e);
router.push('auth/login');
});
}
}, [accessToken]);
refresh 토큰의 값에 변화가 생겼을 때, refreshToken 이 만료되었다면 로그인 페이지로 라우팅 시키고, accessToken 이 만료된 경우엔 refresh 토큰을 이용해 accessToken 재발급을 요청하는 비동기 코드를 작성하였다.
const handleLogout = () => {
Cookies.remove('accessToken');
Cookies.remove('refreshToken');
router.push('auth/login');
};
로그아웃 기능을 구현할 때에는 쿠키에서 access 와 refresh 토큰을 지워주기만 하면 깔끔하게 구현할 수 있다.
이렇게 로그인 기능 구현에 필요한 전반적인 지식들을 알아보고, 이를 바탕으로 간단하게 로그인, 로그아웃 기능을 구현해 보았다. 이 글이 로그인 기능을 처음 구현하는 사람들에게 좋은 이정표로 영향을 끼칠 수 있길 바라며 글을 마친다.
https://velog.io/@hahan/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
https://mygumi.tistory.com/375
https://youngman12.tistory.com/15