코딩 기록소
article thumbnail
반응형

이번에는 토이 프로젝트를 하면서 알아본 JWT에 대해서 얘기를 할 것이다.
JWT에 들어가기 앞서 인증마다 무슨 차이점이 존재하는지 알아보자.

 

Cookie

Cookie란 Key: Value로 이루어진 문자열이다.
사용자가 어떠한 웹사이트에 방문했을 때 그 사이트가 사용하는 서버를 통해 사용자 로컬(하드)에 저장되는 작은 데이터다.
Cookie는 Key : Value로 이루어져 있으며 만료시간, 도메인, 경로 등의 정보를 가질 수 있다.

Cookie 예시

Cookie 작동 방식

https://www.javatpoint.com/cookies-in-servlet

  1. Browser(사용자)가 Server에 요청을 보낸다.
  2. Server는 Browser의 요청에 대한 응답을 작성할 때 Client측에 저장하고싶은 데이터를 응답 헤더의 Set-Cookie에 담아서 응답한다.
  3. Browser는 Cookie를 응답 받았다면 다음 요청을 보낼 때마다 요청 헤더의 Cookie를 실어서 보낸다.
  4. Server는 그러한 Cookie를 보고 사용자 식별, 정보를 확인하며 요청을 수행한다.

Cookie 단점

  • Cookie를 사용하지 않는 단점 중 가장 큰 것은 보안에 취약하다는 점이다. 사용자의 개인 정보를 저장하는 것이기 때문에 개인 정보를 노출이 될 수 있을 뿐만 아니라 XSS 공격, 스니핑 공격으로 인해 쉽게 조작되거나 탈취당할 가능성이 크다.
  • 용량에 제한이 있으므로 많은 정보를 담을 수가 없다.
Xss 공격이란?
웹사이트에 악성 스크립트를 주입하여 악성 스크립트를 포함된 게시글을 열람한 피해자들의 쿠키는 해커로 전송되는 해킹 기법이다. 이를 이용하여 사용자의 세션을 탈취하거나, 웹사이트 변조, 악의전인 컨텐츠 삽입, 피싱 공격 등을 시도할 수 있게 된다.
스니핑 공격이란?
네트워크 상에서 자신이 아닌 다른 사용자의 패킷(데이터) 교환을 도청하는 것이다.

 

Session

Cookie의 보안적인 측면을 해결하고자 사용자의 정보를 사용자 Local에 저장하는 것이 아닌 서버 측에서 저장하고 관리하는 기법이다.
서버에서는 Session 객체에 Key로 사용하는 Session ID와 그에 해당하는 Value로 저장된다.
서버의 읽기 속도를 빠르게 하기 위해 서버의 메모리에 저장하기도 하며, 서버의 로컬 파일이나 데이터베이스에 저장하기도 한다. Session 가장 큰 특징은 사용자가 정보를 가지고 있는 것이 아닌 서버에서 모두 관리한다는 점이다.

 

Session 작동 방식

https://hazelcast.com/glossary/web-session/

  1. Browser(사용자)가 로그인을 한다.
  2. 사용자의 정보를 Session에 저장하고 Session ID를 Browser한테 반환하여 Browser는 그걸 Cookie에 저장하게 된다. 이 때 서버는 Session을 서버 메모리 또는 데이터베이스등에 저장하게 된다.
  3. 이후 Browser는 모든 요청에 Session ID를 담아 요청한다.
  4. 서버는 Session ID에 기반하여 응답을 해준다.

Session 단점

  • Cookie는 탈취를 당하면 사용자의 정보를 위변조할 수 있게되지만, Session은 탈취 당해도 Session ID에는 사용자의 민감한 정보를 담고 있지 않는다. 하지만, Session ID를 통해 사용자인척 위장할 수 있다는 단점이 존재한다. (이러한 단점은 IP를 특정하여 관리, 만료시간을 두어 새로 발급하는 방법 등이 존재한다.)
  • 로그인한 모든 사용자가 서버에 접근하여 Session 서버에 자원을 사용하기 때문에 서버에 트래픽이 많아져 성능을 저하시키게 된다. (이러한 단점은 읽기 속도가 빠른 메모리 서버에 저장하고 Session 서버를 따로 두어 관리한다.)

 

JWT (Json Web Token)

JWT는 사용자가 로그인을 하면 서버는 해당 사용자가 인증(Authentication)이 되었고, 기본적인 정보(민간한 정보 X)토큰으로 만들어 사용자에게 응답을 한다. 사용자는 이 토큰을 기반으로 인가(Authorization)를 필요로 하는 요청에는 Header Authorization에 값으로 토큰을 보내게 된다.

 

JWT 구조

https://jwt.io/

각각 Header, Payload, Signature를 구분하기 위해 . (dot)을 사용한다.

Header (헤더)

Header는 2가지의 정보를 가지게 된다.

  1. alg : 해싱 알고리즘을 지정한다. 해싱 알고리즘은 기본적으로 HS256을 사용한다.
  2. typ : 토큰의 타입을 지정한다. JWT이기 때문에 typ는 JWT 고정이다.

 

Payload (내용)

Payload는 토큰에 담을 정보가 담겨져 있다. 여기에는 정보 하나하나가 클레임(claim)이라 부르고 이 정보들은 Key : Value로 이루어져있다. Payload는 단순히 인코딩만 되어있어 제 3자가 복호화하여 볼 수가 있기 때문에 민감한 정보는 넣지 않는 것이 좋다.

  • 등록된 클레임 (Registered Claim)
    • 등록된 클레임들은 서비스에 필요한 정보가 아닌 토큰에 대한 정보들을 담기위해서 만들어졌으며, 이름이 이미 정해져있는 클레임이다. 모든 등록된 클레임 선택적이다.
이름 설명
iss 토큰 발급자 (issuer)
sub 토큰 제목 (subject)
aud 토큰 대상자 (audience)
exp 토큰의 만료시간 (expiration), 시간은 NumericDate로 이루어져야 하고, 현재 시간보다 이후로 지정되어야 한다.
ex) 1673741869628
nbf 토큰 활성 날짜이며, 시간은 NumericDate로 이루어진다.
이 날짜가 지나기 전까지는 토큰이 활성화 되지 않는다.
iat 토큰이 발급된 시간 (issued at)이며, 토큰이 언제 발급 됐는지를 알 수 있다.
jti JWT 식별자이며, 중복적인 처리를 방지하기 위해 많이 사용된다.
  • 공개 클레임 (Public Claim)
    • 공개 클레임들은 충돌이 방지된 이름을 가지고 있어야하며, 충돌 방지를 위해 클레임 이름을 URL로 짓는다.
    • 단순히 서버와 클라이언트 사이에서 사용자를 인증, 인가하는 용도로 사용한다면 사용하지 않아도 된다. 서버와 사용자 사이의 통신을 넘어 제 3자도 JWT 토큰을 사용할 때 충돌 방지를 위해 합의된 클레임이기 때문이다.
  • 비공개 클레임 (Private Claim)
    • 사용자와 서버 사이에서 사용되는 클레임으로써, 양측 협의 하에 사용되는 클레임 이름을 쓴다.
  • Payload 예시
{
    "iss": "seungyong20.tistory.com",                   // 등록된 클레임 (토큰 발급자)
    "exp": 1673741869628,                               // 등록된 클레임 (토큰의 만료시간)
    "<https://xxx.com/jwt_claims/is_admin>": true,      // 공개 클레임
    "userId": "00001",                                  // 비공개 클레임
    "username": "seungyong"                             // 비공개 클레임
}

 

Signature (서명)

Signature은 Header인코딩 값과 Payload 인코딩 값을 합친 후 주어진 Secret Key로 헤더에 지정된 알고리즘을 통해 해쉬를 하여 생성한다. 그리고 만들어진 해쉬값을 base64로 인코딩하여 나타낸다.

Header와 Payload는 인코딩된 값입니다.
Header와 Payload는 인코딩된 값이기 때문에 제 3자가 복호화 및 정보를 조작할 수 있지만 Signature는 서버 측에서 Sercret Key를 관리하고 있기 때문에 Sercret Key가 유출되지 않는한 토큰의 위변조를 막을 수 있다.

JWT 작동 방식

https://www.vaadata.com/blog/jwt-tokens-and-security-working-principles-and-use-cases/

  1. Browser(사용자)가 로그인을 한다.
  2. 서버는 사용자가 로그인에 성공하면 User ID 등을 Secret Key로 생성한다.
  3. 서버는 만들어진 JWT를 사용자에게 반환한다.
  4. 사용자는 인가를 필요로 하는 요청 Authorization Header에 Token을 넣어 요청한다.
  5. 서버는 Token을 기반으로 사용자의 정보를 가져온다.
  6. 서버는 Token으로 가져온 사용자의 정보를 기반하여 응답을 해준다.

JWT 장점

  • 사용자 인증에 필요한 정보는 토큰 자체에 포함하기 때문에 별도의 서버가 필요가 없다. (stateless)
  • 서버를 사용하지 않음으로써, 트래픽에 대한 부담이 낮다.

JWT 단점

  • 토큰 자체에 사용자 정보를 인코딩하여 저장하기 때문에 탈취를 당하면 Payload의 정보가 공개된다.
  • 토큰에 정보가 많아지면 토큰의 크기가 증가하므로 네트워크에 부하를 줄 수 있다.

 

JWT의 Access Token과 Refresh Token

제 3자에게 Token을 탈취 당한다면 서버는 탈취 당한 사실 여부를 알 수 없기에 Access Token, Refresh Token을 나누어 보안성을 강화한다.

Access Token

Access Token은 사용자의 정보를 담고 있는 토큰으로써, 사용자 인증 / 인가를 위한 토큰이다.
서버는 Access Token을 받으면 토큰의 정보를 사용하여 사용자 정보를 제공해주는 역할을 한다.
Access Token은 탈취를 당했을 때를 대비해 만료시간을 짧게 잡는다. 보통 30분정도를 잡는다.

Refresh Token

Access Token의 만료시간이 다 되면 새로운 토큰을 발급해주는 토큰이다. 제 3자를 통해 Access Token을 탈취 당해도 만료시간이 지나면 새로운 토큰을 발급하기 때문에 보안성을 조금이라도 강화시킨다.
Refresh Token은 Access Token을 새로 발급하기 위한 수단으로 Access Token보다 만료시간을 길게 잡기 때문에 보통 14일 정도를 잡는다.
Access Token의 비밀키와 Refresh Token의 비밀키는 다르게 설정하는 것이 좋다.

Access Token과 Refresh Token 작동 방식

https://apim.docs.wso2.com/en/3.2.0/learn/api-security/oauth2/grant-types/refresh-token-grant/

  1. 사용자가 로그인 시도 또는 인증 서버에 요청을 한다.
  2. 서버는 로그인을 성공하면 Access Token과 Refresh Token을 반환해준다.
  3. Access Token을 사용하여 요청을 한다.
  4. Access Token에 정보를 기반하여 응답한다.
  5. 만료된 Access Token을 보낸다.
  6. 서버는 만료된 Access Token이라고 401(Unauthorization)를 반환한다.
  7. 만료된 Access Token이라는걸 안 사용자는 인증 서버에 Access, Refresh Token을 보낸다.
  8. 서버는 만료된 Access Token과 Refresh Token이 문제가 없다면 새로운 Access Token을 발급한다.
Access Token과 Refresh Token 둘 다 만료가 되었다면?
둘 다 만료되어 쓸 수 없는 Token이 되었으므로, 사용자는 새로 로그인을 하여 Token을 발급 받아야합니다.
하지만, Access Token을 새로 발급 받으면 Refresh Token을 새로 발급 받는 기법(RTR)을 사용하기도 합니다. 이 방법은 좀 더 밑에서 자세히 알아봅시다.

 

RTR (Refresh Token Rotation)

벌써 JWT에 대한 마지막 주제에 다가왔다. 마지막으로 알아볼 RTR 기법을 간단하게 말하자면 Refresh Token을 단 한번만 사용하게 일회용 토큰으로 사용하는 기법이다.

 

Access Token과 Refresh Token 보안 문제

만약 Access Token과 Refresh Token이 둘 다 탈취가 되었다면?이라는 생각으로부터 시작됐다. 두 개의 토큰은 서버가 관리하지 않는 상태(stateless)여서 탈취 여부를 알 수 없기 때문에 만료시간이 될 때까지 기다릴 수 밖에 없다. 물론 사용자로부터 도용 신고가 들어오면 Log를 통해 찾아낼 수 있으나 그건 신고가 들어와 log를 찾아내는 과정을 해야 한다.

 

RTR 작동 원리

https://pragmaticwebsecurity.com/articles/oauthoidc/refresh-token-protection-implications.html

  1. Access Token 1과 Refresh Token 1을 얻는다.
  2. Refresh Token 1을 사용하여 Access Token 2와 Refresh Token 2를 얻는다.
  3. Refresh Token 2를 사용하여 Access Token 3와 Refresh Token 3를 얻는다.
  4. 이 과정을 계속 반복한다.

 

그래서 내가 보안을 위해 해결한 방법은?

  • Token을 빠르게 처리하기 위해 Redis(인메모리 데이터베이스)를 사용했다.
  • 보안성을 강화하였으니 다음은 제 3자가 공격했을 때의 시나리오다.
    • Access Token이 무조건 만료되어야 새로운 Token을 발급 받을 수 있다. 만약 그러지 아니할시 Access, Refresh Token을 폐기시킨다. 제 3자가 Refresh Token을 사용하여 일찍 Access Token을 발급받을라 하는 상황을 막을 수 있다.
    • Access Token을 탈취하더라도 짧은 시간을 가진 Access Token은 Refresh Token을 통해 새로 발급 받아야한다.
    • 만약 제 3자가 탈취한 Refresh Token으로 만료된 Access Token을 들고 새로 요청해서 발급 받더라도 기존 사용자는 새로 발급된 Access, Refresh Token이 아니기 때문에 Redis에 있는 Refresh Token과 일치하지 않아 다시 로그인을 해야한다.
    • 하지만 휴면상태로 들어간 사용자는 탈취 당했다는 정보는 알 수 없다.
  • 이러한 방법은 JWT의 Stateless 방식에 어긋나는 행동이기도 한다.
  • Access Token이 있을 때는 Redis에 접근하지 않기 때문에 서버에 요청하는 횟수는 Session보다 적긴 하다는 특징이 있다.

 

마무리하며...

사실상 완벽한 보안은 없다. 인증 방식에는 서로서로 장단점이 존재하며, 상황에 맞게 사용해야할 거 같다.
Browser와 Server 둘 다 보안에 좀 더 신경쓰고 대비를 해야하는 것이 최선인 것 같다.
그래도 이번 JWT를 공부하면서 Cookie, Session, Token 방식 인증에 대해서 많은 지식을 얻어갔다.

반응형
profile

코딩 기록소

@seungyong20

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!