개발

쿠키 / 세션 / 토큰(JWT) 의 차이는?

whiporithm 2023. 9. 27. 02:04

 

서론

개발하다보면 로그인을 구현할때가 많다. 로그인 구현하는거야 쉬우나, 해당 유저가 로그인이 된 유저인지, 아닌지를 판단하는게 중요하다. http는 기본적으로 stateless 프로토콜이기에 이전에 접속한 클라이언트의 정보를 기억하지 않는다. 따라서 요청때 판단을 해야한다. 간단하게 생각해보면 모든 요청때 유저 정보를 보낼 수가 있을것이다. (쿼리 스트링과 같은 형태로 )  당연히 이 방법은 보안적으로도 안좋고, 모든 요청에 유저 정보에 대한 파라미터를 받아야 하므로 개발도 힘들어질 것이다. 

 

이런 문제들을 해결하기 위하여 쿠키, 세션, 토큰 등의 방식이 등장했고, 이들을 활용하여 권한이 있는 유저인지 판단한다.
개발할때는 부랴부랴 찾아서 어떻게든 구현 하는 편인데, 개념이 헷갈려서 정리해볼려고 한다.

 

 

인증 / 인가

 

위에서 설명한 쿠키, 세션, 토큰등의 테크닉은 "인가" 를 위한 프로세스이다. 때문에 인증과 인가의 차이를 짚고 가겠다.

 

인증(Authentication)

  • 인증은 단순하게 말하면 해당 유저가 회원가입이 되어있냐를 검증하는 과정이다.
  • 이 유저가 우리 서비스에 등록되어있는 회원인지를 검사하는 프로세스이다. (주로 로그인 과정)

 

인가(Authorization)

  • 인가는 인증된 회원이 특정 서비스에 접근할때 그 권한이 있는지 검사하는 프로세스이다.
  • 특정 서비스를 접근할때 이 회원이 "로그인 되어있는지", "이 서비스를 이용할 자격이 있는지" 를 검사한다

인가로 단순히 로그인 여부를 체크할 수도 있겠지만, 때로는 그 유저의 역할(등급)에 따라 처리해줄수도 있을 것이다.

 

 

쿠키

 

MDN 문서에는 쿠키는 이렇게 정의되어 있다.

 

" HTTP 쿠키(웹 쿠키, 브라우저 쿠키)는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각입니다."

 

기본적으로 쿠키는 웹브라우저가 지니고 있으며, 서버측에서 쿠키 데이터를 설정함으로써 지닐 수 있다. 특정 요청을 클라이언트 측에서 하면, 서버측에서 쿠키를 설정해줌으로써 브라우저는 쿠키를 지니고 있는것이다.

 

 

위의 사진을 보면 이해할 수 있다. 로그인 요청을 보내고, 그 처리가 제대로 된다면 서버측에서 쿠키를 설정해준다. (set cookie)  그렇게 되면 브라우저의 쿠키 저장소에 쿠키가 저장된다. 

 

이렇게 유저정보를 클라이언트에서 들고 있다면 요청때 이를 함께 보냄으로써 서버에서는 "인가" 를 처리할 수 있는것이다. (이놈이 로그인 되어있는 놈인지 아닌지 말이다.)

 

중요한 점은 쿠키는 기본적으로 모든 서버 요청에 함께 동봉되어진다.  웹 브라우저는 요청을 하기전에 항상 쿠키 저장소를 확인하고 요청을 보내기 때문이다. 이는 편할수도 있지만 네트워크 트래픽을 유발할 수 있다. 선택적으로 데이터를 보내려면 웹 스토리지와 같은 또다른 클라이언트 저장소를 활용해야 한다.  

 

 

개발자 도구의 application탭을 들어가보면 cookie 정보를 확인할 수 있다.

 

쿠키의 생명주기

쿠키에게는 만료일이 존재한다. 서버측에서는 Expires 또는 max-age로 시간초를 정할 수 있다. 개발자도구에도 아래와 같이 쿠키가 만료되는 시점을 표현하고 있다. 

  • Expires 같은 경우에는 날짜와 시간으로 표현함으로써 만료되는 시기를 표현한다. (아래 사진을 보면 알 수 있다.)
  • max-age 같은 경우에는 "n초" 를 설정함으로써 설정한 시간이후로 n초가 지나면 쿠키가 만료되도록 설정한다.

 

 

이 때 만료 날짜를 생략하면 브라우저 종료시까지 유지하는 세션쿠키가 되며,

만료 날짜를 입력하면 해당 날짜까지 유지되는 영속쿠키가 된다.

 

expires가 명시되지 않은 세션쿠키는 Session이라고 표기된다.

쿠키를 이용해서 로그인 유지를 구현하였고, 브라우저를 닫았을때 로그아웃 된다면 그것은 세션쿠키를 사용한 것이다.

 

 

(쿠키와 관련한 정보는 더욱 많고, 여기서는 로그인과 관련된 정보를 다루었지만 쿠키는 광고 트래킹에도 많이 활용된다. 하지만 이 글에서는 쿠키에 대한 내용은 이정도만 작성하고, 세션으로 넘어가보겠다.)

 

 

세션 

위에서 쿠키에는 유저의 정보를 쌩으로 넣어서 검증을 시도했다. 이와같이 정보를 그냥 노출시키는것은 "당연히" 보안상 좋지 않다. 탈취되었을경우에 그 정보가 모두 해커에게 넘어가기 때문이다. 

 

이와 같은 상황을 방지하기 위해서 세션을 사용하게 되었다. 세션이란 유저마다 세션ID를 부여하고, 그 정보를 서버가 지님으로써 해당 유저에게 인가해줄것인지 판단하는 기술이다. (세션 ID는 프로젝트에 따라 어떻게 생성할지 기준을 달리 할 수 있겠다.)

 

로그인을 하게되면 서버에서 고유의 세션ID를 생성하고 저장소 (속도면에서 주로 메모리를 많이 사용하지만, 특정 DB가 될수도 있다.) 에 저장해둔다. 그리고 그 값을 유저에게 넘겨준다. 

 

그리고 그 유저는 해당 값을 가지고 있다가 요청할때 보내줌으로써 "인가" 를 받게 된다.

 

 

대충 위와 같은 그림이 될텐데, 위에서는 세션 ID를 반환한다고 표시해뒀지만 세션 ID는 주로 쿠키에 저장해서 활용한다. 그러나 이는 절대적인 룰이 아니기에 위와 같이 표현했고, 세션ID를 어디에 저장해서 관리할지는 프로젝트에 따라 다를것이다. 

 

 

세션의 장단점

  • 장점
    • 서버가 유저를 관리할 수 있다. 만약 세션ID가 탈취되었을 경우에는, 해당 세션ID를 저장소에서 삭제함으로써 인가를 불허할 수 있다.
  • 단점
    • 서버의 오버헤드가 심해진다. 아무래도 서버측에서 세션ID를 모두 관리하고 있어야하기에 유저가 많아진다면 이 부하는 계속 커질 수 밖에 없다.
    • 세션 불일치 이슈가 있다. 이는 여러 서버를 운영하며 로드밸런싱 과정에서 일어나는 이슈인데, 각 서버마다 세션의 정보가 일치하지 않기에 발생하는 이슈이다. (이와 관련해서도 토픽이 꽤 크기 때문에 이정도로만 설명하겠다.)

 

 

토큰

서버측에서 세션ID를 모두 관리하고 있고 그 오버헤드가 커진다는 단점때문에 토큰 방식이 등장했다. 

 

우선 세션과 토큰의 차이점을 비유로 알아보자. 

 

파티를 주최한다고 생각해보자. 요청한 사람에 한해서 우리는 티켓을 나눠줄것이다.

이때 세션방식은 요청이 들어왔을 경우에 티켓을 발급하고, 그 복사본을 고객에게 전달해준다. 원본 티켓은 주최측 창고에 저장해둔다. 이후 고객이 파티에 입장할때 고객이 제시한 복사본 티켓을 받고, 창고에 있는 원본 티켓하고 비교해서 입장을 허가해준다.

 

토큰같은 경우에는 요청이 들어오면 티켓을 발급해준다, 단 티켓에 주최측만 알 수 있는 진품여부를 표시해두고, 고객에게 전달한다. 이후 고객이 티켓을 제시했을때 그 티켓의 진품여부를 판별하고 입장을 허가해준다.

 

여기서 차이는 "창고"의 차이다. 토큰같은 경우에는 고객이 들고있다가 주최측에서 검사만하면 되기때문에 복잡함이 훨 떨어지고 간결해진다. 

 

대충 이런 구조이다.

 

토큰 방식은 주로 JWT (Json Web Token) 을 많이 활용하는데 이 방식을 한 번 알아보자.

 

 

JWT

JWT는 3가지 부분으로 구성되어 있다. header, payload, signature로 구성되어진다.

 

 

header는 해당 토큰의 타입(여기서는 JWT), 그리고 암호화된 알고리즘의 종류가 들어간다.

payload에는 해당 토큰이 언제 만료되는지, 간단한 유저의 정보등 claim이라 부르는 데이터가 들어간다. (payload에는 서버측에서 여러 정보를 담을 수 있는데 이는 토큰에서 단순히 base64로 감싸지기에 민감한정보를 넣으면 안된다.)

 

header와 payload는 각각 base64로 인코딩 되어서 표현되고 그 사이는 '.' 으로 연결된다.

 

그리고 마지막으로 signature가 있다. signature는 base64로 인코딩된 header와 payload를 연결해서 암호화를 한 값이 된다. 이 때 암호화에 사용되는 키는 서버측만 아는 시크릿키이다.  JWT 암호화에는 주로 HS256방식을 사용한다고 하며, 암호화와 복호화에 모두 사용되는 대칭키 시스템을 사용한다. 

 

정말 간략하게 java로 구현하면 아래와 같다. (단 암호화는 구현하기 번거로워 하는척만 했다..)

 

import java.util.Base64;

public class JWT {
    public String secretKey = "thisissecretkey";

    public String makeSignature(String data){
        return data+secretKey;
    }

    public String makeJWT(String header, String payload){
        String header_encode = Base64.getEncoder().encodeToString(header.getBytes());
        String payload_encode = Base64.getEncoder().encodeToString(payload.getBytes());
        String signature = makeSignature(header_encode+"."+payload_encode);

        return header_encode+"."+payload_encode+"."+signature;
    }

    public static void main(String[] args) {
        JWT jwt = new JWT();
        String header = "header";
        String payload = "payload";
        System.out.println(jwt.makeJWT(header, payload)); //서버가 전달해주는 토큰
    }
}

결과는 이렇게 나오고, 이것이 유저가 받는 토큰이다.

 

* 실제로는 header나 payload는 위에서 보인것처럼 json 형태로 있지만 편의상 String으로 표현했다.

* 암호화는 뒤에 키만 더해주는 방식으로 하는척만 구현했으니 적당히 이해하자.

 

 

그러면 변조는 어떻게 감지할까?

 

만약 해커가 payload에 접근하여 값을 바꾸었다고 해보자. 그리고 서버로 보냈다.

서버는 전달된 토큰의 signature를 복호화해서 header와 payload를 비교해본다. 이때, payload가 기존과는 달라진것을 알 수 있고 정상적이지 않은 토큰임을 감지할 수 있다. (결국 signature가 위에서 말한 진품인지를 판별하는 기준이 된다.)

 

(JWT또한 어디에 저장할지는 프로젝트에서 정의함에 따라 다르겠다. 쿠키에 설정할수도 있고, 따로 클라이언트 저장소를 사용할수도 있겠다. 단 요청할때는 주로 http header에 세팅해서 보내는 편인거 같다. )

 

JWT의 장단점

  • 장점
    • 서버의 부하가 줄어든다. 토큰을 유저들이 가지고 있기에 서버에서는 토큰을 검증하는 로직만 존재하면 된다.
  • 단점
    • 보안에 취약하다. 세션같은 경우에는 탈취되었을경우 세션저장소에서 삭제하는 등의 방법이 가능하나, 토큰은 그것이 불가능하다. 탈취 사실을 알고, 만약 진품여부의 기준을 바꾼다면 여태 발급된 모든 토큰은 무효화 된다. 이를 로그인에 빗대서 말해보면 모든 사용자가 갑자기 로그아웃되는 것이다.  (refresh token등의 보안 대처 방법이 있으나 원론적으로는!)

 

 


 

 

결론

세션과 jwt모두 각각의 장단점이 존재하기에 상황에 맞추어서 사용하는게 베스트이다. 위에서는 정말 단순하고 원론적인 이야기들을 적어두었기에 여러 상황을 고려해서 선택할 필요가 있다. (좀 더 깊은 내용은 공부를 더해서 정리해볼려고 한다. 보안과 관련된 부분이 상당수기에..) 

 

 

참조

https://kbwplace.tistory.com/165

https://brunch.co.kr/@jinyoungchoi95/1

https://github.com/boojongmin/memo/issues/7