스프링 시큐리티를 사용하려면 세션과 JWT 토큰에 대해서 이해해야한다.
세션과 JWT 토큰은 우리가 평소에 말하는 로그인에서 아주 중요한 역할을 한다. 사실 로그인 그 자체로 봐도 무방하다.
우리가 로그인을 하면 그 로그인 내용을 가지고 글을 쓰거나, 글으 수정하거나, 댓글을 다는 행위들을 한다. 어떻게 컴퓨터는 우리가 로그인한 사용자인 것을 알까? 바로 세션ID와 JWT 토큰이 그 역할을 해준다.
(악필인 점을 감안해주세요)
우리는 어떤 서버에 로그인을 한다고 가정합시다.
1. 네이버를 예로 들자면 사용자가 네이버 서버에 로그인 요청을 할 때 네이버 ID와 Password를 입력합니다.
2. 네이버 서버가 해당 ID와 Password에 대한 '인증' 단계를 거치고 세션 저장소에 해당 내용을 담은 세션을 저장합니다(저장한 것을 세션ID라고 하고 보통 난수의 이름으로 저장).
3. 그 세션ID=123을 HTTP Response의 쿠키로 설정하여 사용자에게 보낸다(스프링 부트의 경우 JSESSIONID).
4. 이 후에 권한이 필요한 요청(글쓰기)를 서버에 보낼 때 해당 세션ID를 받은 서버는 그 세션ID가 세션 저장소에 있는지 확인하고 '인가'의 여부를 결정합니다.
내용이 어렵나요? 결국 우리가 생각하는 로그인 상태를 유지하는 것은 이 세션ID를 쿠키값으로 가지고 있냐 없냐의 차이입니다. 우리가 로그인 한 상태에서 브라우저 쿠키 초기화를 해버리면 다시 로그인을 해야합니다. 이 것은 발급받은 세션 ID를 초기화 시켜버렸기 때문이죠.
세션은 제일 간단하고 실용적인 로그인 방식입니다. 하지만 간단하면 꼭 단점이 있죠?
세션의 단점은 사용자가 많을 때 발생합니다.
만약 우리가 만든 서비스가 너무 잘되서 이렇게 하나의 서버로는 사용자가 감당이 안된다고 생각해봅시다.
그렇다면 우리는 여러대의 서버를 사용해야되고 로드밸런싱을 해야합니다. 하지만 세션 저장소는 로드밸런싱을 했을 때 공유를 하지 않습니다.
만약 우리가 위 그림과 같이 로그인을 했던 서버가 1번이라고 하면 다음 '인가' 절차를 거칠 때 1번 서버가 아닌 2,3번 서버로 접근하게되면 '인가'가 불가능해지겠죠?
자 이런 상황 일 때 어떤 해결 방법이 있을까요?
DB를 사용해서도 해결이 가능할 것 같습니다. DB에 '인증' 절차를 완료한 내용을 저장해두고 요청이 날라올 때마다 DB를 통해 확인 하는 것이죠.
하지만 이 방법에도 문제가 있습니다. DB에 접근하는 것 자체가 네트워크와 하드디스크 IO가 일어난다는 점이죠. 우리가 CS과목을 공부하면서 배우지만 CPU의 캐시메모리, RAM에 저장된 내용을 참조하는데는 시간이 오래걸리지 않습니다. 하지만 하드디스크와 네트워크를 탄다면 그 시간이 훨씬 길어지는 것을 알고 있습니다.
DB를 활용하는 방법도 문제라니... 그러면 해결법이 도대체 뭘까요?
우리는 세션의 문제를 JWT 토큰을 통해서 해결할 것입니다.
JWT토큰은 Header, Payload, Signature의 3 부분으로 이루어지며, Json 형태인 각 부분은 Base64로 인코딩 되어 표현된다. 또한 각각의 부분을 이어 주기 위해 . 구분자를 사용하여 구분한다. 추가로 Base64는 암호화된 문자열이 아니고, 같은 문자열에 대해 항상 같은 인코딩 문자열을 반환한다.
해당 사이트는 JWT의 공식 사이트이다. JWT 토큰은 그림과 같이 .으로 구분되어진 3개의 파트가 있다.
토큰의 헤더는 typ과 alg 두 가지 정보로 구성된다. alg는 헤더(Header)를 암호화 하는 것이 아니고, Signature를 해싱하기 위한 알고리즘을 지정하는 것이다.
토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다. 클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있다.
클레임에는 크게 등록된 클레임과, 사용자가 등록하는 클레임 두개로 나눌 수 있다.
등록된 클레임은 위와 같이 이미 형식이 지정된 약속같은 값들이고, 사용자 클레임은 사용자가 임의로 값들을 넣을 수 있다.
토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 서명(Signature)은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.
JWT 토큰과 세션의 가장 큰 차이점은 '인가'정보를 누가 담고 있냐이다. 세션은 서버의 세션 저장소에 해당 인증 내용을 담았지만, JWT 토큰을 활용하면 각 로드밸런싱한 서버에 토큰 검증 클래스나 메소드만 존재하면 '인가' 과정까지 해결이 가능하다.
또한 보통의 JWT 인증 과정은 AccessToken과 RefreshToken을 이용한다.
나 같은 경우에 사용자가 회원가입을 하거나, RereshToken이 없이 로그인을 완료하면 RefreshToken을 발급하고 그 것을 DB에 저장해둔다. 그럼 다음 로그인부터는 사용자가 AccessToken의 기한이 지나거나 사용하지 못하게 되었을 때 RefreshToken을 첨부하여 요청하면 ID-Password 검증 없이 바로 요청한 RefreshToken을 통해서 AccessToken을 발급한다.
이렇게 좋은 JWT 토큰을 이용한 로그인 방식에도 문제가 있다. 스프링 시큐리티를 JWT토큰과 사용하려면 결국 시큐리티 세션에 Authentication을 담아야한다.
이 부분의 해결 방법으로는 스프링 시큐리티 자체를 사용하지않고 자신만의 시큐리티를 만드는 것이 있다고 생각한다.
프로젝트의 기간이 길다면 한번 도전 해보고 싶지만 기간이 그리 길지 않아서 우리는 JWT토큰을 이용해 Authentication을 찾고, 이 것을 SecurityContextHolder에 담아서 권한처리까지 해보자.