세션과 쿠키 그리고 토큰 ver2

최지환·2023년 2월 13일
0

스프링

목록 보기
12/12
post-thumbnail

예전에 쿠키와 세션에 관련해서 간단하게 알아보고, 정리를 한 경험이 있다. 이 당시에는 강의를 통해서 배운 내용을 간단하게 정리만 했었다.

이번에 프로젝트를 하면서 로그인 기능 구현을 하며, 인증 방식에 대한 고민이 조금 있었다.

당연하게 세션으로 적용을 하면된다고 생각했고, 일단 해보자라는 마인드로 적용을 하려니 조금 난감했다. 세션을 기반으로 구현을 해보는 도중, 제대로 하고 있는건가? 라는 의문이 들었다. 그리고 세션 적용을 위해 자료를 찾다보면 요즘에 많이 쓴다는 토큰방식 이라는 것도 나오게 되면서 내가 인증 관련 부분을 너무 모른다는 생각이 들었다.

그리고 이런 과정에서 이런 개념들을 다시 공부해보라는 조언도 얻게 되었다.

그래서 다시 원점으로 돌아와서 쿠키, 세션, 토큰 이 세가지 개념을 정리해보고 프로젝트에 어떤 것을 적용해야할지 고민하기로 했다.

해당글인 이전에 쿠키와 세션에 대해 정리한 글을 보안하여 작성하였다.

쿠키와 세션


인증 방식

인증 방식에는 크게 쿠키, 세션, 토큰 으로 3가지 방식이 있다.

이런 인증 방식은 HTTP의 2가지 특성으로 인해서 고안되었다.

  • 비연결성 - 클라이언트와 서버가 한번 연결 한 후, 클라이언트 요청에 대해 서버가 응답을 마치면 연결을 끊어버리는 성질
  • 무상태성 - 서버는 클라이언트를 식별할 수 없는 성질

비연결성과 무상태성으로 인해, 서버 입장에서는 클라이언트로 부터 들어온 요청에 대한 상태와 이전 통신의 상태가 남아 있지 않는다.
쉽게 말해 인증이 필요한 웹 사이트에서 페이지를 넘어가는 상황에서, 클라이언트가 서버로 요청을 보내면, 서버는 해당 요청을 매번 인증을 해야하는 문제가 발생한다.
극단적으로 브라우저에서 새로고침을 할때마다 매번 로그인을 다시 해야하는 상황이 발생한다.

이런 비연결성과 무상태성을 특징을 보안할 수 있던 것은 Cookie 와 Session 덕분이다.

쿠키는 클라이언트가 어떤 웹 사이트를 방문하였을 때, 서버를 통해서 클라이언트의 브라우저에 저장되는 기록이나 정보이다.

스프링에서 쿠키를 생성 할 때는 new Cookie()를 통해 만들 수 있다.

이후 responseaddCookie() 를 해 응답 요청에 쿠키를 넣어주면 된다.

예시 - 로그인 후, 아이디를 쿠키에 넣어 응답요청으로 반환한다.

@PostMapping("/login")
    public String loginForm(@Validated @ModelAttribute("loginForm") LoginForm form, BindingResult bindingResult, HttpServletResponse response) {

  //...

        //로그인 성공 처리
        //쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시  모두 종료)
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);

        return "redirect:/";
    }

응답 헤더 확인 - 브라우저의 개발자 모드로 확인

다음과 같이 쿠키에 memberId 가 1 으로 지정 되어 있는 것을 확인 할 수 있다.

이제 브라우저는 서버에 요청을 보낼 때 memberId=1 이라는 쿠키로 서버에 자신의 로그인 정보를 알릴 수 있다. 마치 상태를 갖고 있는 것처럼 요청을 보낼 수 있다.

이후 서버는 요청을 받을 때, 이런 memberId = 1이 라는 쿠키정보를 통해서 해당 요청이 어떤 유저의 요청인지 확인 할 수 있다.

쉽게 말해, 쿠키를 사용하여 로그인 id (memberId=1)을 전달해 로그인을 유지 할 수 있다. 하지만 이런 방식은 보안적으로 심각한 문제가 있다.

보안 문제

  • 쿠키 값은 임의로 클라이언트에서 변경 할 수 있다.
    • 쿠키에 지금과 같이 id 값을 갖고 있으면 해커가 이를 탈취 할 수 있다는 문제점이 있다.
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
    • 이런 로그인 로직을 처리하는 중요한 값이 웹 브라우저에 보관되고, 네트워크 요청마다 클라이언트에서 서버로 전달이 된다. → 해커가 클라이언트, 네트워크 전송 구간 등 이를 해킹할 범위가 확대된다.

⇒ 이런 문제를 해결하기 위해 세션을 이용한다.


서버에 중요한 정보를 따로 보관하고 연결을 유지하는 방법

→ 중요한 정보를 서버에 저장하고, 클라이언트에는 이에 대한 키 값을 쿠키에 넣어 전송하는 방식

즉 세션 ID를 쿠키에 넣어 클라이언트에게 보내고, 중요한 정보는 서버내에 저장.

세션의 동작 방식

  1. 로그인 처리 요청이 들어오면 서버는 해당 요청이 올바른 요청인지 확인한다.(아이디와 패스워드가 맞는지)

  1. 해당 사용자가 맞으면 세션 id를 생성하여 세션 저장소에 저장한다. 이때 세션 id가 key, 보관할 값(memberA)은 value로 저장한다.

💡💡💡💡
세션 Id는 UUID로 생성한다. UUID(범용 고유 식별자 universally unique identifier) 는 중복될 가능성이 거의 없다. UUID.*randomUUID*().toString(); 으로 uuid 스트링을 생성할 수 있다.
UUID 예시
550e8400-e29b-41d4-a716-446655440000

  1. 클라이언트는 쿠키 저장소에 세션ID를 저장한다.
    여기서 중요한 포인트는 회원과 관련된 정보는 클라이언트에 직접 전달이 되지 않는다.

  1. 이후 클라이언트가 요청을 보내면, 쿠키 저장소에 있는 세션 값이 담긴 쿠키를 보내고, 서버는 이를 받아 자신의 세션 저장소를 조회한다. 이때 세션id를 비교하여 로그인시 보관한 세션 정보를 사용한다.

세션 저장소

세션 저장소는 말 그대로 세션을 관리한다고 생각하면 된다. 세션 인증 방식에서 이런 세션 저장소는 필수적인데, 이로 인해 발생하는 문제도 있다.

세션 저장소는 기본적으로 내장 톰캣의 메모리에 저장이된다. 그렇기 때문에 애플리케이션 서버가 재시작이 될 때, 세션은 초기화가 된다. 만약 2대 이상의 서버를 사용한다면 각 서버의 톰캣마다 세션을 동기화 해주어 씽크를 맞춰야하는 불편함이있다.

이렇게 멀티 서버 환경에서 발생하는 문제를 해결하기 위해서는 크게 3가지 방식이 있다.

  1. 톰캣 세션을 사용
  • 일반적으로 별 다른 설정을 하지 않는 때 기본적으로 선택되는 방식
  • 톰캣(WAS)에 세션이 저장되기 때문에 2대 이상의 WAS 가 구동되는 환경에서는 톰캣들 간 세션 공유를 위한 설정이 필요
  1. MySql과 같은 데이터베이스를 세션 저장소로 사용
    • 여러 WAS 간 공용 세션을 사용하기 위해 DB에 세션을 관리
    • 로그인 요청이나 인증 요청마다 DB IO 이슈가 발생하기 때문에 성능상 문제가 발생할 수 있음.
  2. Redis 와 같은 메모리 DB를 세션 저장소로 사용
    • B2C 서비스에서 가장 많이 사용하는 방식
    • AWS에서 서비스를 운영하고 배포한다면 Redis와 같은 메모리 DB는 별도로 사용료를 지불해야함.

요약

  • 세션을 사용해서 서버에서 중요한 정보를 서버에서 관리하고, 클라이언트에게 전송을 하지 않을 수 있다.
    → 보안 ⬆️
    - 쿠키 값을 변조 하더라도, 서버의 세션 저장소에 매핑된 키값과 다르기 때문에 회원정보에 접근 불가
    - 쿠키를 탈취 후 세션을 사용하더라도, 시간이 지나면 사용할 수 없도록, 서버에서 세션의 만료시간을 설정할 수 있다.
  • 서버에서 세션값을 관리하기 위한 세션 저장소를 사용하기 때문에, 요청이 많아지게 되면 서버에 과부하가 올 수 있음.
  • 세션 저장소가 세션 & 쿠키 인증 방식에서 중요하기 때문에 관리가 필요
  • 특히 멀티서버 환경에서는 서버간 세션 저장소의 동기화 작업을 해줘야함.

JWT 기반 인증 - 토큰 인증

클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증 되었다는 의미로 ‘토큰’을 부여한다.

토큰은 유일 하다는 특징을 갖고 있다. 토큰을 발급받은 클라이언트는 또 다시 서버에 요청을 보낼 때, 요청 헤더에 이전에 서버로부터 받은 토큰을 넣어 보낸다.

그러면 서버는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰인지 체크하고, 인증을 처리한다.

얼핏 보면 세션(쿠키 & 세션) 인증 기반 방식과 비슷하다고 생각될 수 있다.

하지만 차이점이 있다.

기존 세션 기반 인증은 서버가 세션에 대한 정보를 가지고 있고, 이를 사용 할 때마다, 조회 하는 과정이 필요하다.

이와 다르게 토큰은 이런 정보가 서버가 아닌 클라이언트에 저장 되기 때문에, 서버에 부담을 줄일 수 있다.

토큰 자체에 정보가 들어있기 때문에, 서버입장에서는 클라이언트에서 토큰을 받아 위조되었는지 안되었는지 판별만 하면 된다.

특히 토큰은 쿠키와 세션이 없는 앱에서 많이 사용된다.


토큰 인증 방식

  1. 클라이언트가 로그인 요청을 하면, 로그인이 일치하면 서버는 토큰을 생성해서 응답에 토큰을 심어 클라이언트에게 보낸다.
  2. 클라이언트는 토큰을 통해 서버로 부터 받은 정보를 접근할 수 있고, 이후 요청시 서버로부터 받은 토큰을 담아서 서버에 요청한다.
  3. 서버는 클라이언트로 부터 받은 토큰을 검증하여 인증을 하고, 클러이언트에게 응답을 보낸다.

토큰 방식의 단점

  • 쿠키/세션 인증 방식과 다르게 토큰 자체의 데이터 길이가 길기 때문에, 인증 요청이 많아진다면 네트워크 부하가 심해진다.
  • Payload 자체는 암호화가 되지 않기 때문에 유저의 중요한 정보를 담을 수 없다. → Payload는 뒤에서 다루겠다.
  • 토큰을 탈취 당하면 대처가 불가능하다. → 이는 토큰에 사용 기간 제한을 설정하는 식으로 극복할 수 있으나, 완벽하게 해결은 불가하다.

JWT (JSON Web Token)

JSON 웹 토큰 ( JSON Web Token, JWT)는 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준이다. 해당 양식은 헤더, 페이로드, 서명의 구조로 이루어져 있다.

JWT는 인증에 필요한 정보들을 토큰에 담아 암호화 시켜 사용한다.
기본적으로 Cookie, Session과는 다르게 서명된 토큰이라는 특징이 있다.


JWT 구조

jwt 는 헤더, 페이로드, 서명을 나타내는 3가지의 문자열을 . 구분자로 가지고 있습니다.

헤더 (Header)

Header는 두가지 정보를 가지고 있다.

  • typ : 토큰의 타입을 지정 , JWT 이다.
  • alg : 해싱 알고리즘을 지정. 일반적으로 HMAC SHA256 혹은 RSA 가 사용됩니다.
    토큰을 검증 할 때 signature 부분에서 사용된다.

Header 예제

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

정보(payload)

Payload 부분에는 토큰에 담을 정보가 들어있다. Payload에 담는 정보 ‘한 조각’을 클레임(claim) 이라고 부르고, 한 클레임은 name:value 한 쌍으로 이루어져있다. 토큰에는 여러개의 클레임들을 넣을 수 있다.

클레임의 종류는 등록된(registerd) 클레임, 공개(public) 클레임, 비공개 (private) 클레임으로 크게 3가지로 분류된다.

  • 등록된(registerd) 클레임 : 서비스에서 필요한 정보가 아닌, 토큰에 대한 정보들을 담기위해 이미 정해진 클레임들, 등록된 클레임은 선택적으로 사용한다.
    • 토큰 발급자, 토큰 제목, 토큰 대상자, 토큰 만료시간, 토큰 발급 시간 등에 대한 정보를 넣을 수 있다.
  • 공개 클레임 : 충돌이 방지된 이름을 가져야함. 따라서 URI 형식으로 이름을 지음
  • 비공개 클레임 : 등록된 클레임도 아니고, 공개 클레임도 아닌 것. 보통 클라이언트와 서버간 협의하에 사용하는 클레임 이름들이다. 이름이 중복되면 충돌이 발생할 수 있기 때문에 주의해야함.

PayLoad 예제

{
		"iss": "hello.com", // 발급자
    "exp": "1485270000000", //토큰 만료시간
    "https://velopert.com/jwt_claims/is_admin": true, // 공개 클레임
    "userId": "11028373727102", //비공개 클레임
    "username": "velopert" // 비공개 클레임
}

서명(signature)

서명(signature)은 인코딩된 Header 값과 Payload를 더한 뒤 비밀키로 해싱하여 생성한다. Header 와 Payload는 단순히 인코딩된 값이기 때문에, 제 3자가 복호화하면 조작을 할 수 있다. 하지만 Signature는 서버 측에서 관리하는 비밀키를 알아야 복호화 할 수 있다. 따라서 Signature는 서버가 클라이언트로 부터 받은 토큰에 대해 위변조 여부를 확인할 때 사용한다.

장점

  1. 세션과 다르게 인증 정보에 대한 별도의 저장소가 필요가 없다.
  2. JWT 방식의 토큰은 토큰 자체에 기본 정보, 전달할 정보, 토큰에 대한 서명 등 필요한 정보를 다 가지고 있다.
  3. 세션 인증방식과 다르게 서버는 무상태가 됨.
  4. 확장성 우수 (쿠기 세션을 지원하지 않는 앱에서도 사용 가능)

단점

  1. 쿠키 & 세션과 다르게 JWT는 토큰이 길어 인증 요청이 많아질 수록 네트워크 부하가 심해짐
  2. 기본적으로 Payload 자체는 암호화 되더라도, 탈취하여 암호화를 풀 수 있음
    → 따라서 추가적으로 payload에 부분 암호화를 적용하여 토큰을 발급하면 암호화가 가능
  3. 토큰이 탈취되면 대처하기가 어려움

보안 전략

  1. 짧은 토큰 만료료 기한 설정
  • 토큰이 탈취되더라도 빠르게 만료시혀 피해를 최소화 할 수 있음. 하지만 사용자가 토큰이 만료될 때 마다 인증을 재요청해야함.
  1. Sliding Session
  • 예를 들어 사용자가 글 작성 도중 토큰이 만료가 되면, 작성한 글을 전송 할때, 작업이 정상적으로 처리되지 않고, 이전에 작성한 글이 날아갈 수 있음. 따라서 서비스를 지속적으로 사용하는 클라이언트에게 자동으로 토큰 만료 기한을 늘려 줄 수 있음.
  1. Refresh Token
  • 클라이언트가 인증 요청을 보내면 서버는 Access Token 와 만료 기한이 더 긴 Refresh Token을 발급하는 전략이다. 클라이언트는 Access Token이 만료되었을 때 Refresh Token을 사용하여 Access Token의 재발급을 요청한다. 서버는 DB에 저장된 Refresh Token과 비요하여 유효하면 새로운 Access 토큰을 발급한다.
    → 이 경우 Refresh Token을 별도의 storage에 저장해야한다. 따라서 I/O 작업이 일어나기 떄문에 JWT의 장점(I/O 작업이 없는 인증 처리)의 효과를 제대로 누릴 수 없다.

0개의 댓글