[JWT] 로그인 인증 및 토큰 관리에 대해서

Y_Y·2024년 3월 10일
0
post-thumbnail

서론

팀 스파르타 회사의 프로덕트 캠프 2기를 진행하면서 느낀 점들을 바탕으로 보안에 대해서도 중요하다고 생각하여 이 글을 작성합니다.

먼저 간략하게 과정을 소개하자면,

ZERO TO ONE, 12주 간 현업과 동일한 팀 구성으로 진행하고, 실제 사용자를 모집하여 창업까지!

라는 목표로 PO, Designer, FE, BE 각 1명씩 총 4명 한 팀으로 진행하였습니다.

이에 따라서 MVP 배포 이후 사용자를 모집하고 실제 유저들이 유입이 되는 상황에서 문득 지난 뉴스들이 스쳐 지나갔습니다.

모 회사에서 얼마동안 개인정보가 유출이 돼 수만명의 개인정보가 유출되었다!
어느회사 서비스의 어떤 파일이 보안취약점이 발견돼 신속한 대응을 해야한다!
라는 뉴스가 심심치 않게 종종 들렸었습니다.

이런 이유로 저 또한 심지어 아직 상용서비스화 된 것도 아닌 서비스에서 회원가입한 고객들에게 피해가 가면 안되겠다라는 생각으로 기존 로그인 인증 방식을 수정해야할 필요성을 느꼈습니다.

JWT 란

JWT : JSON Web Token

JSON 형태의 객체 정보를 암호화하여 웹 인증에 사용할 수 있는 토큰.

구성으로는 Header, Payload, Signature로 aaaaa.bbbbb.ccccc와 같은 형태를 띄고 있다.

헤더에는 토큰 타입, 토큰을 암호화하는 방식이 담겨있습니다.

{
  "alg": "HS256",
  "typ": "JWT"
}

예시로 이 JSON은 Base64Url로 인코딩되어 헤더를 구성합니다

Payload

페이로드는 Claim이라는 사용자에 대한 프로퍼티나 속성을 담고 있습니다.
그리고 claim에는 3가지 타입이 존재하는데, registered, public, and private 타입이 존재합니다.

claim 방식은 요청받는 서버입장에서 페이로드 값을 통해 사용자에 대한 정보를 다른 곳에서 더 가져올 필요가 없다는 것이다.

registered claim

registered claim은 유용하고 상호 운용 가능한 클레임 집합을 제공하기 위해 필수는 아니지만 권장되는 미리 정의된 클레임 집합입니다.
예시로 iss (발행자), exp (만료 시간), sub (주제), aud (청중) 등이 있는데 3글자로 구성되어 있습니다.

public claim

public claim은 JWT를 사용하는 사람들이 원하는 대로 정의할 수 있습니다. 그러나 충돌을 방지하려면 IANA JSON 웹 토큰 레지스트리 에 정의하거나충돌 방지 네임스페이스를 포함하는 URI로 정의해야 합니다.

private claim

private claim은 사용에 동의한 당사자 간에 정보를 공유하기 위해 생성된 맞춤 클레임으로, 등록되거나 공개 된 클레임이 아닙니다.

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

예시로 이 정보는 마찬가지로 Base64Url로 인코딩되어 페이로드를 구성합니다.

NOTE : 헤더나 페이로드는 단순히 Base64Url로 인코딩 되어 있기 때문에 개인정보와 같은 보안에 민감한 정보는 담지 않는 것이 좋다.

추가적으로 그렇다면 토큰의 장점이 세션과 다르게 DB와 같이 다른 곳에서 정보를 확인하는 절차를 거치지 않는다는 것이 있는데, 결국 사용자 정보를 조회해야 하는 경우 마찬가지로 DB에 접근해야하지 않는가? 라는 질문을 할 수 있을 것이다.

물론 그런 경우에는 내 생각에는 필요한 것이라고 생각한다. 하지만 로그인한 유저에게만 보여줄 수 있는 페이지의 경우 해당 토큰만 가지고 라우팅을 컨트롤할 수 있기 때문에 조금 더 부하를 줄일 수 있지 않을까 생각한다.

Signature

Signature는 인코딩된 헤더와 페이로드 그리고 시크릿 키를 통해 암호화하여 저장하여 해당 시크릿 키(대칭 키)를 통해 토큰이 위변조 되었는지 확인할 수 있다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

특징

  1. jwt방식은 특정 경우에 state-less할 수 있다.
    -> 내 경우에 Refresh Token을 DB에 저장하여, Access Token을 발급할 때 사용하여 state-ful, Access Token의 경우 서버와 통신에만 사용하기 때문에 이 경우에는 state-less하다.
  2. 토큰만 사용하여 사용자 인증을 진행할 수 있다.
    -> 추가적인 서버/DB에 접근할 필요가 없어 서버의 부하를 줄일 수 있다.
  3. 일부 서버에서 8KB의 제한이 있을 수 있기 때문에 가급적 크기를 작게 해야한다.
    -> 불필요한 데이터를 넣지 않아야한다.
  4. 세션 방식은 탈취된 경우 세션을 만료시켜 추가적인 피해를 막을 수 있지만, 토큰 방식은 탈취된 토큰을 만료시킬 방법이 없다. -> 주기적인 갱신이 필요하다. -> 안정성은 높아지지만 지속적인 자원을 소모하게 된다.

JWT 공식 사이트에서 제공해주는 다이어그램으로 JWT 발급, 인증 방식에 대해서 보여주고 있습니다.

본론

기존 방식

단순히 JWT 방식을 사용하여 프론트에선 Access Token, Refresh Token을 쿠키에 저장하여 Access Token 하나만 사용하고 백엔드에선 토큰을 DB에 저장하여 비교하는 토큰을 사용한 세션 방식을 사용해왔습니다.

하지만 사용자가 하나 둘 늘어남에 따라 지난 날 보안과 관련하여 뉴스에 개인정보유출에 대해서 보도가 되었던게 생각이 났습니다.

그래서 개인정보보호를 위해 인증에 대해서 보안을 향상시켜야 할 필요성을 느끼게 되었고 더욱 알아보기로 하였습니다.

개선하려는 방식

Access Token과 Refresh Token을 용도에 맞게 사용한다.
-> Access Token을 인가를 위한 용도로, Refresh Token은 Access Token을 재발급 하는 용도로 사용한다.

그리고 보관하는 방식을 쿠키에 담아두는게 아닌 Access Token을 메모리에, Refresh Token은 쿠키에 저장한다.

또한 옵션으로 httponly, secure, samesite등을 설정하여 추가적으로 보안을 향상시킨다.

이유

이렇게 하는 이유는 메모리에 Access Token을 저장할 경우 휘발되는 성격을 활용해 보안을 향상할 수 있다고 생각한다.
그리고 쿠키에 Refresh Token을 저장하는 이유는 통신할 때 쿠키에 담긴 Refresh Token을 자동으로 보내 만료 여부와 오염 여부를 확인하여 재발급을 진행하기 위해 이러한 방식을 사용하려고 한다.

과정

Access Token을 메모리에 저장하려는 과정에서 사용하던 상태관리 라이브러리를 통해 저장하려고 하였으나, 저장이 되지 않아 결국 local storage에 Access token을 저장하고, 쿠키에만 Refresh token을 저장하였다.

결과

Refresh Token의 생명주기에 따라 미리 silent fresh 로직을 추가해 사용자가 모르게 Access Token을 재발급하도록 하여 상황에 따라 Access Token, Refresh Token을 재발급하여 사용할 수 있게 하였다, 만약 새로고침이나 모종의 이유로 인해 해당 로직이 재수행이 안될 경우 Axios interceptor를 사용하여 Access Token의 만료 여부를 확인하여 재발급한 토큰을 가져와 통신을 이어가도록 진행하여 UX를 개선하였다.

결론

작성한 시점이 많은 시간이 지난 후여서 미흡한 부분이 많지만 이후 다시 토큰을 사용하는 로직을 만들어야할 경우 이 경험을 바탕으로 더욱 공부하고 보완하여 개발할 수 있도록 해야겠다.

출처

JWT 공식사이트
프론트에서-안전하게-로그인-처리하기
https://12bme.tistory.com/130
https://puleugo.tistory.com/138

profile
남을 위해(나를 위해) 글을 쓰는 Velog

0개의 댓글