JSON 웹 토큰이란?
JWT(JSON Web Token)는 웹 표준으로 정의된 토큰 기반의 인증 및 권한 부여 방식 중 하나입니다.
주로 사용자 인증에 활용되며, 특히 웹 및 모바일 애플리케이션에서 널리 사용됩니다.
본론으로 들어가기 전 인증(Authentication)과 인가(Authorization)에 대해 먼저 알아보겠습니다.
즉, 어떤 사이트나 서비스에 등록한 사용자가 자신의 신원을 확인하고 서비스에 접근하는 것을 의미합니다. 이를 위해 사용자는 아이디와 패스워드 등의 자격 증명을 제공하게 되고, 시스템은 이를 확인해 사용자의 신원을 검증합니다.
즉, 인가는 인증된 사용자에게 특정 리소스 또는 서비스에 대한 접근 권한을 부여하는 과정을 의미합니다. 아는 사용자가 로그인되어 있는 상태에서 자신의 계정으로만 할 수 있는 특정 활동이나 리소스에 접근할 때 발생합니다.
인증이 사용자의 신원을 확인하고 시스템에 로그인하는 것이라면, 인가는 로그인된 사용자가 어떤 특정한 권한을 가지고 있는지 확인하고, 해당 권한에 따라 특정 리소스에 접근을 허용 또는 거부하는 것입니다.
JWT는 인증보단 인가에 연관된 기술
{"alg": "HS256", "typ": "JWT"}
와 같은 형태를 가집니다.클라이언트가 서버에 로그인 요청을 보내면, 서버는 해당 사용자에 대한 정보를 기반으로 JWT를 생성하고, 이를 클라이언트에게 반환합니다. 클라이언트는 이 JWT를 저장하고, 이후의 요청에서는 이 토큰을 함께 서버에 전달하여 인증을 수행합니다.
JWT를 사용함으로써 서버는 세션 저장소를 관리할 필요 없이 클라이언트에서 토큰을 관리하므로, 분산된 서비스 아키텍처에서도 유용하게 사용될 수 있습니다.
사용자가 로그인에 성공하면 세션이라는 티켓을 출력해줍니다.
그럼 이 티켓을 반으로 나누어 반은 사용자, 반은 메모리(경우에 따라 하드디스크나 DB 등에 담아두기도 함)에 보관합니다.
브라우저는 메모리에 보관한 티켓을 session id란 이름의 쿠키로 저장하고, 이 브라우저는 사이트에 요청을 보낼 때마다 이 티켓을 실어서 보냅니다.
session id를 사용해 사용자가 서버에 로그인 되어 있음이 지속되는 상태를 세션이라고 합니다.
즉, 사용자가 현재 로그인되어 있는 지를 갖고서 서버가 사용자가 로그인에 성공했을 때 출력한 티켓의 반쪽을 어디에 보관했는지(메모리, DB 등) 기억을 해두고 있는 것이 세션입니다.
단점 : 만약 메모리에 티켓을 보관했을 경우, 많은 사용자가 접속했을 경우 메모리가 부족해질 수 있습니다. 또 메모리는 휘발성이기 때문에 서버에 문제가 생겨 메모리가 꺼진다면 보관해둔 티켓은 사라집니다.
이렇게 된다면 로그인이 풀리며, 사용자는 다시 로그인해야 되는 상황이 발생하는데,
그럼 하드디스크나 DB에 저장하면 안되나요?
이 둘은 메모리에 비해 속도가 느려진다는 단점이 있습니다.
이러한 부담 없이 인가를 구현하기 위해 고안된 것이 바로 토큰 방식, JWT 입니다.
토큰이란?
jwt 방식을 사용하면 사용자가 로그인 했을 때 세션과 같이 토큰이라는 티켓을 출력해주는데, 세션과 다른 점은 이 티켓을 온전히 사용자에게 전달합니다. (서버가 기억하지 않음)
사용자가 받은 토큰은 인코딩 또는 암호화된 3가지 데이터를 이어붙인 것입니다.
토큰을 확인하면 중간 중간에 마침표(.)가 있는데, 이 마침표를 기준으로 3부분으로 나뉩니다.
예시) XXXX.YYYY.ZZZZ
위 토큰은 각각 header(헤더)
, payload(페이로드)
, verify signature(서명)
로 구분됩니다.
header
: 헤더를 디코딩해보면 두 가지 정보가 담겨있습니다.payload
: 페이로드를 디코딩해보면 json 형식의 여러 정보들이 들어있습니다.세션처럼 stateful(상태 유지)해서 모든 사용자들의 상태를 기억하고 있다는 건 구현하기 부담되고 고려사항도 많습니다.
하지만, 이게 가능하기만 하다면 기억하는 대상의 상태들을 언제든 제어할 수 있다는 장점이 있는데,
예를 들어 만약 한 기기에서만 로그인 가능한 서비스를 만든다고 한다면,
pc에서 로그인한 상태의 사용자가 모바일에서 또 다시 로그인을 시도 한다면 pc에서는 로그아웃 되도록 기존 세션을 종료할 수 있습니다.
하지만 JWT는 사용자에게 토큰을 쥐어주는 방식이기 때문에 제어하기가 힘듭니다.
다른 문제로는 만약 토큰을 해커에게 탈취 당했을 경우 해당 토큰을 무효화할 방법도 없기 때문에 실제 서비스를 운영하는 곳에서는 JWT만으로 인가를 구현하는 곳은 많지 않습니다.
이런 문제점을 보완하기 위해 만료 시간을 짧게 잡아 토큰의 수명을 줄이는 방법이 있습니다.
그럼 얼마 지나지 않아서 또 로그인해야 되지 않나요?
수명이 짧은 access 토큰과 수명이 긴 refresh 토큰이 있습니다.
사용자는 로그인을 하면 이 access 토큰과 refresh 토큰을 받습니다.
서버는 access 토큰과 refresh 토큰을 발급하고 클라이언트에게 보내고 나서 refresh 토큰은 상응값을 DB에도 저장합니다.
사용자는 access 토큰의 수명이 다 하면 refresh 토큰을 보내는데, 서버는 이걸 DB에 저장된 값과 대조해보고 맞다면 새 access 토큰을 발급해줍니다.
즉, 매번 인가를 받을 때 사용하는 수명이 짧은 토큰이 access 토큰이고, access 토큰을 재발급 받을 때 사용하는 것이 refresh 토큰입니다.
이렇게 하면 access 토큰이 탈취 당했을 경우 수명이 짧은 access 토큰의 특성상 오래 사용하지 못하며, 탈취 당한 사용자의 로그아웃을 이행하기 위해 refresh 토큰을 이용해 DB에서 상응값을 지워버려 토큰 갱신이 실패하게 만들어 로그아웃 시킬 수 있습니다.
구체적인 상황 및 서버의 구현에 따라 조금씩 다르겠지만 주로 사용하는 로직의 예시를 알아보겠습니다.
// 로그인 후 토큰 저장
const setAccessToken = (token) => {
localStorage.setItem("accessToken", token);
};
const axiosWithAuth = axios.create({
baseURL: // api 주소,
headers: {
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
}
})
위 로직에서 처럼 직접 localStorage.getItem('accessToken')
을 사용하는 방식은 해당 로직을 여러 곳에서 중복해서 사용해야 하며, 나중에 변경이 필요할 때 모든 사용처를 찾아 수정해야 하는 불상사가 생깁니다. 이는 유지보수에 어려움을 줄 수 있기 때문에 재사용성을 위해 분리합니다.
토큰 관리하기 :
const TOKEN = "accessToken";
const TokenRepository = {
// token 저장
setToken(token) {
localStorage.setItem(TOKEN, token);
},
// token 반환()
getToken() {
return localStorage.getItem(TOKEN);
},
// token tkrwp
removeToken() {
localStorage.removeToken(TOKEN);
},
};
export default TokenRepository;
setToken(token)
: 발급 받은 token
을 localStorage
에 accessToken
이라는 키로 저장합니다. 이렇게 저장된 토큰은 브라우저 세션 간에 유지되며, 나중에 필요할 때 다시 사용될 수 있습니다.getToken()
: localStorage
에서 accessToken
키에 저장된 토큰을 가져와 반환합니다. 다른 부분에서 이 함수를 호출하여 현재 사용 중인 토큰을 확인할 수 있습니다.removeToken()
: localStorage
에서 accessToken
키에 저장된 토큰을 제거 합니다. 주로 사용자가 로그아웃할 때나 토큰이 더 이상 유효하지 않을 때 호출됩니다.export const axiosInstance = axios.create({
baseURL: // api 주소,
// ... 추가 로직
})
export const axiosWithAuth = config = > {
const access_token = TokenRepository.getToken()
if(access_token) {
config.headers.Authorization = `Bearer ${access_token}`
}
return config
}
axiosInstance.interceptors.request.use(axiosWithAuth)
axiosWithAuth
는 axios 요청의 설정(config)을 받아와서 해당 설정의 헤더에 Authorization 필드를 추가하고, 이때 헤더에는 TokenRepository.getToken()
을 통해 가져온 accessToken
이 포함됩니다.axiosInstance.interceptors.request.use(axiosWithAuth)
를 사용해 axiosInstance로 api 요청이 발생하기 전 axiosWithAuth
함수가 실행되도록 설정합니다.즉, 모든 api 요청이 보내지기 전에 인증 헤더가 추가되도록 중앙에서 처리합니다.
// 로그인 후 Refresh Token 저장
const setRefreshToken = (token) => {
localStorage.setItem("refreshToken", token);
};
const refreshAccessToken = async () => {
const refreshToken = localStorage.getItem("refreshToken");
try {
const res = await axios.post("api 주소", {
refreshToken,
});
const newAccessToken = res.data.accessToken;
setAccessToken(newAccessToken);
// 갱신된 토큰을 사용하여 이후 API 요청 수행
axiosWithAuth.get("/api/endpoint");
} catch (err) {
// Refresh token 이 만료되었을 경우, 로그인 페이지로 리다이렉트 등의 처리
console.error("Failed to refresh access token: ", err);
}
};