본문 바로가기
  • ANALOG CODE
  • AnalogCode
개발

JWT를 활용한 로그인 인증 구현

by 아날로그코더 2023. 3. 8.
반응형

JWT

 

로그인 인증을 구현하기 위해 Session 과 JWT 방식 중에 엄청난 고민을 하다가, 

아무래도 DB를 덜 사용하고 MSA에 유리한 JWT 방식을 사용하기로 마음먹었다.

 

개발 코드: Vue, NuxtJS

 

1. JWT 인증 최종 구현 모델

JWT에 대한 세부적인 스펙과 관련한 설명은 생략하도록 하겠다.

JWT 토큰을 이용해 어떻게 안전하게 인증 시스템을 만들어야 하는지에 초첨을 두었다.

 

🤜 일단 결론적으로 아래 그림이 내가 구현한 인증 모델이다. 크게 3가지 부분으로 나뉜다.

 ① 로그인

 일단 로그인을 요청하면 사용자 정보를 확인하고 JWT를 사용하여 AccessToken과 RefreshToken을 발급한다.

 

👉 토큰 만료시간

 만료시간을 AccessToken은 짧게, RefreshToken은 길게 잡는다.

 이를 통해 우리는 AccessToken이 탈취되더라도 짧은 만료시간을 통해 보안을 강화할 수 있다.

 

👉 RefreshToken DB 저장

RefreshToken은 DB에 저장한다.

그리고 DB에 저장된 RefreshToken을 통해 서버에서 의심이 되는 토큰을 무효화시킬 수 있다.

 

👉 RefreshToken 전달

RefreshToken은 쿠키로 전달한다. 이때 httpOnly 옵션을 주면 브라우저에서 Javascript로 접근이 불가능하다.

 

👉 AccessToken 전달

AccessToken은 쿠키로 전달하지 않는다. 이유는 CSRF 공격을 막기 위해서이다.

HTTP 응답의 Body로 전달하여 클라이언트에서 안전하게 저장하도록 한다.

 

👉 AccessToken 저장

응답으로 받은 AccessToken값을 브라우저 어딘가에 저장해야 한다.

 

사실 쿠키에 저장하는게 개발적으로는 가장 편하다. 별도의 코드없이 그냥 인증이 필요한 API를 호출하면 알아서 인증정보가 포함되기 때문이다. 하지만 아까도 말했든 CSRF 공격에 취약하다.

 

LocalStorage와 SessionStorage 모두 CSRF 공격에는 안전하다. 하지만 둘다 모두 자바스크립트 코드를 통해 접근이 가능하기 때문에 XSS 공격에는 취약하다.

저장소 XSS CSRF 장점 단점
메모리 안전 안전 보안에 안전 새로고침시 토큰 유지(X)
API 호출시 토큰처리 필요
쿠키 안전(단, httpOnly 옵션) 취약 새로고침시 토큰 유지
API 호출시 토큰처리 불필요
CSRF 공격에 취약
LocalStorage 취약 안전 새로고침시 토큰 유지 API 호출시 토큰처리 필요
SessionStorage 취약 안전   API 호출시 토큰처리 필요
브라우저 종료시 토큰 유지(X)

보안상 가장 안전한 곳은 메모리이다.

물론 쿠키를 사용해서 CSRF Token 을 이용하면 보안을 강화할 수 있다. 하지만 서버/클라 모두 추가적인 구현이 필요하다는 단점이 있다.

 

👉 AccessToken 메모리 저장

결국 나는 메모리에 저장하였다. Vuex Store를 통해 저장하고 접근하도록 하였다.

SPA 형태로 개발되었기 때문에 로그인 이후에 페이지 새로고침을 한다고 하더라도 RefreshToken은 쿠키에 남이있기 때문에 최초 로딩시에 Refresh를 호출함으로써 AccessToken을 받아와서 저장하게 하였다.

 

 

 

② 인증이 필요한 API 호출 

중요한 것은 AccessToken 으로만 검증을 한다는 것이다.

RefreshToken은 이 단계에서는 사용하지 않고, 오로지 AccessToken을 발급하는 용도로만 사용해야 한다.

AccessToken이 없는데 RefreshToken이 유효하다고 API 인증을 통과시켜버리면 절대 안된다.

 

👉 Axios Interceptor 

axios 의 interceptor를 통해 모든 요청의 헤더에 Bearer token 값을 넣어주도록 한다.

export default function ({ $axios, store }) {
  $axios.onRequest((config) => {
    if (store.getters.accessToken) {
      config.headers.Authorization = 'Bearer ' + store.getters.accessToken
    }
  })
 }

 

③ 토큰 Refresh

👉 사이트 최초 접속시

 refresh API를 호출하여 accessToken을 받아온다.

 이것을 NuxtJS Vuex Store의 nuxtServerInit 을 통해 구현하였다.

async nuxtServerInit ({ dispatch }) {
  await dispatch('refresh')
},

 

👉 AccessToken 만료시

 API에서 토큰 만료 에러가 나면 refresh API를 호출하고 다시 원래의 API를 호출하도록 구현하였다.

axios interceptor 를 이용하였다.

$axios.onError(async (error) => {
  if (error.response && error.response.status === 401 && !error.config._dont_retry) {
    error.config._dont_retry = true
    if (await store.dispatch('refresh')) {
      return await $axios.request(error.config)
    }
    throw error
  }
  throw error
})

 

 

2. XSS 공격

XSS (Cross Site Scripting) 공격은 공격자가 자바스크립트 코드를 실행되게 만들어서 공격하는 방식이다.

예를 들면 토큰을 탈취해서 공격자의 서버로 정보를 전송하는 스크립트를 게시글에 포함시키고 사용자가 게시글을 클릭하면 사용자의 토큰 정보가 공격자에게로 전달되게 된다.

 

이것을 막기 위해 자바스크립트로 토큰을 가져올 수 있는 방법을 모두 없앴다.

 

👉 AccessToken은 메모리에 저장

👉 RefreshToken은 httpOnly 쿠키에 저장

 

3. CSRF 공격

CSRF (Cross Site Request Forgery) 는 권한을 도용하여 인증된 사용자가 의도하지 않게 서버로 특정 요청을 보내게 하는 것이다.

단순하게 예를 들면 사용자가 페이스북에 로그인된 상태인데 공격자가 사용자에게 탈퇴하는 API 링크를 전달하여 클릭하게 유도하면, 브라우저에서는 이미 로그인된 상태로 인증에 필요한 쿠키값이 있기 때문에 API가 호출되면 인증이 성공해서 동작하게 된다.

 

이것을 막기위해 AccessToken이 자동으로 서버로 전달되지 않도록 하였다.

👉 AccessToken은 메모리에 저장

 

 

반응형

댓글