예전에 쿠키와 세션에 관련해서 간단하게 알아보고, 정리를 한 경험이 있다. 이 당시에는 강의를 통해서 배운 내용을 간단하게 정리만 했었다.
이번에 프로젝트를 하면서 로그인 기능 구현을 하며, 인증
방식에 대한 고민이 조금 있었다.
당연하게 세션으로 적용을 하면된다고 생각했고, 일단 해보자라는 마인드로 적용을 하려니 조금 난감했다. 세션을 기반으로 구현을 해보는 도중, 제대로 하고 있는건가? 라는 의문이 들었다. 그리고 세션 적용을 위해 자료를 찾다보면 요즘에 많이 쓴다는 토큰방식 이라는 것도 나오게 되면서 내가 인증 관련 부분을 너무 모른다는 생각이 들었다.
그리고 이런 과정에서 이런 개념들을 다시 공부해보라는 조언도 얻게 되었다.
그래서 다시 원점으로 돌아와서 쿠키, 세션, 토큰 이 세가지 개념을 정리해보고 프로젝트에 어떤 것을 적용해야할지 고민하기로 했다.
해당글인 이전에 쿠키와 세션에 대해 정리한 글을 보안하여 작성하였다.
인증 방식에는 크게 쿠키, 세션, 토큰 으로 3가지 방식이 있다.
이런 인증 방식은 HTTP의 2가지 특성으로 인해서 고안되었다.
비연결성과 무상태성으로 인해, 서버 입장에서는 클라이언트로 부터 들어온 요청에 대한 상태와 이전 통신의 상태가 남아 있지 않는다.
쉽게 말해 인증이 필요한 웹 사이트에서 페이지를 넘어가는 상황에서, 클라이언트가 서버로 요청을 보내면, 서버는 해당 요청을 매번 인증을 해야하는 문제가 발생한다.
극단적으로 브라우저에서 새로고침을 할때마다 매번 로그인을 다시 해야하는 상황이 발생한다.
이런 비연결성과 무상태성을 특징을 보안할 수 있던 것은 Cookie 와 Session 덕분이다.
쿠키는 클라이언트가 어떤 웹 사이트를 방문하였을 때, 서버를 통해서 클라이언트의 브라우저에 저장되는 기록이나 정보이다.
스프링에서 쿠키를 생성 할 때는 new Cookie()
를 통해 만들 수 있다.
이후 response
에 addCookie()
를 해 응답 요청에 쿠키를 넣어주면 된다.
예시 - 로그인 후, 아이디를 쿠키에 넣어 응답요청으로 반환한다.
@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는 UUID로 생성한다. UUID(범용 고유 식별자 universally unique identifier) 는 중복될 가능성이 거의 없다.UUID.*randomUUID*().toString();
으로 uuid 스트링을 생성할 수 있다.
UUID 예시
550e8400-e29b-41d4-a716-446655440000
세션 저장소는 말 그대로 세션을 관리한다고 생각하면 된다. 세션 인증 방식에서 이런 세션 저장소는 필수적인데, 이로 인해 발생하는 문제도 있다.
세션 저장소는 기본적으로 내장 톰캣의 메모리에 저장이된다. 그렇기 때문에 애플리케이션 서버가 재시작이 될 때, 세션은 초기화가 된다. 만약 2대 이상의 서버를 사용한다면 각 서버의 톰캣마다 세션을 동기화 해주어 씽크를 맞춰야하는 불편함이있다.
이렇게 멀티 서버 환경에서 발생하는 문제를 해결하기 위해서는 크게 3가지 방식이 있다.
클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증 되었다는 의미로 ‘토큰’을 부여한다.
토큰은 유일
하다는 특징을 갖고 있다. 토큰을 발급받은 클라이언트는 또 다시 서버에 요청을 보낼 때, 요청 헤더에 이전에 서버로부터 받은 토큰을 넣어 보낸다.
그러면 서버는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰인지 체크하고, 인증을 처리한다.
얼핏 보면 세션(쿠키 & 세션) 인증 기반 방식과 비슷하다고 생각될 수 있다.
하지만 차이점이 있다.
기존 세션 기반 인증은 서버가 세션에 대한 정보를 가지고 있고, 이를 사용 할 때마다, 조회
하는 과정이 필요하다.
이와 다르게 토큰은 이런 정보가 서버가 아닌 클라이언트
에 저장 되기 때문에, 서버에 부담을 줄일 수 있다.
토큰 자체에 정보가 들어있기 때문에, 서버입장에서는 클라이언트에서 토큰을 받아 위조되었는지 안되었는지 판별만 하면 된다.
특히 토큰은 쿠키와 세션이 없는 앱에서 많이 사용된다.
JSON 웹 토큰 ( JSON Web Token, JWT)는 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준이다. 해당 양식은 헤더, 페이로드, 서명의 구조로 이루어져 있다.
JWT는 인증에 필요한 정보들을 토큰에 담아 암호화 시켜 사용한다.
기본적으로 Cookie, Session과는 다르게 서명된 토큰
이라는 특징이 있다.
jwt 는 헤더, 페이로드, 서명을 나타내는 3가지의 문자열을 .
구분자로 가지고 있습니다.
Header는 두가지 정보를 가지고 있다.
Header 예제
{
"alg" : "HS256",
"typ" : "JWT"
}
Payload 부분에는 토큰에 담을 정보가 들어있다. Payload에 담는 정보 ‘한 조각’을 클레임(claim) 이라고 부르고, 한 클레임은 name:value
한 쌍으로 이루어져있다. 토큰에는 여러개의 클레임들을 넣을 수 있다.
클레임의 종류는 등록된(registerd) 클레임, 공개(public) 클레임, 비공개 (private) 클레임으로 크게 3가지로 분류된다.
PayLoad 예제
{
"iss": "hello.com", // 발급자
"exp": "1485270000000", //토큰 만료시간
"https://velopert.com/jwt_claims/is_admin": true, // 공개 클레임
"userId": "11028373727102", //비공개 클레임
"username": "velopert" // 비공개 클레임
}
서명(signature)은 인코딩된 Header 값과 Payload를 더한 뒤 비밀키
로 해싱하여 생성한다. Header 와 Payload는 단순히 인코딩된 값이기 때문에, 제 3자가 복호화하면 조작을 할 수 있다. 하지만 Signature는 서버 측에서 관리하는 비밀키를 알아야 복호화 할 수 있다. 따라서 Signature는 서버가 클라이언트로 부터 받은 토큰에 대해 위변조 여부
를 확인할 때 사용한다.