Session - Stateful
Spring Security는 기본적으로 formLogin을 통한 Session 방식 인증/인가를 지원합니다. 다른 처리 없이 UserDetails와 UserDetailsService를 구현하고 로그인하면 JSESSIONID라는 쿠키가 발급되는 걸 볼 수 있습니다. 로그인된 사용자의 정보는 서블릿 컨테이너의 Session Storage에 저장되어 사용되는데 이처럼 사용자 정보를 서버가 기억해두는 것을 stateful이라고 표현합니다. 요청이 인증 관련 쿠키를 포함하고 있으면 쿠키에 담긴 키를 통해 session에 접근하고 로그인된 사용자인지 검증합니다. 프로젝트 규모가 작고 트래픽이 크지 않을 경우, 간편하게 구현할 수 있는 세션 방식 로그인은 좋은 선택지 중 하나일 수 있습니다.
그러나 대규모 트래픽을 감당하는 서버를 만들어야 한다면 session방식 로그인의 stateful 속성은 서버의 부담을 증가시킵니다. 서버가 로그인된 사용자들의 정보를 모두 기억하고, 매 요청마다 session storage를 확인해서 사용해야 되기 때문입니다.
세션 방식 인증의 요청 흐름을 표현하면 아래와 같습니다.
Token - Stateless
이는 Token 방식 로그인을 채택함으로써 일부 해소할 수 있습니다. 세션 방식과 다르게 Token방식 로그인은 사용자의 State를 Client에게 위임합니다. 서버는 사용자의 로그인 정보를 기억하지 않고, 토큰을 가지고 접근하는 요청에 대해 해당 토큰이 유효한지 검사하는 방식으로 인증을 처리합니다. 전체적인 요청 흐름은 아래와 같습니다. 토큰 검증은 Security FilterChain의 커스텀 필터에서 일어나게 됩니다.
아래는 개인 프로젝트에서 JWT인증 방식을 도입했을 때의 검증 과정입니다. 토큰을 파싱하여 내부 데이터만 확인하면 사용자의 인증 정보를 알 수 있기 때문에 별도의 저장소가 필요하지 않습니다.
토큰의 검증(JWT)
임의로 발급한 JWT를 파싱한 모습입니다. 사용자 정보인 sub, 권한 정보인 role을 비롯해 발급, 만료 일자가 담겨있는 것을 볼 수 있습니다. JWT는 Base64로 인코딩 되기 때문에 누구나 쉽게 내용을 보고, 심지어는 만들 수도 있습니다. 이렇게 보면 인증에 사용하기에 토큰은 적합하지 않아 보입니다. 인코딩이 쉬운 만큼 아주 약간의 정보만 있어도 서버에서 발급한 것처럼 보이는 토큰을 만들 수 있을 것 같으니까요. 그러나 JWT를 위조하는 건 굉장히 어려운 일입니다. 그 이유는 JWT가 가지고 있는 서명(Signature)에 있습니다.
JWT는 헤더, 페이로드, 시그니처 세 요소로 구성되어 있습니다. 각 요소는 구분자인 . 으로 구분됩니다. 위 토큰에선 푸른색으로 구성된 마지막 부분이 시그니처에 해당합니다. 서명은 헤더와 페이로드 정보를 바탕으로 서버의 비밀 키와 해시 알고리즘을 통해 생성되기 때문에 다른 정보를 모두 알아도 비밀 키를 모른다면 유효한 서명 정보를 만들 수 없습니다.
유효한 서명은 그대로 두고 토큰에 담긴 사용자 정보만 조작하여 사용하고 싶어도 마찬가지입니다. 시그니처 생성에 헤더, 페이로드의 정보가 모두 사용되기 때문에 페이로드의 내용이 바뀌면 서명 값도 바뀌어야 합니다. 결국 비밀키만 노출되지 않으면 조작된 토큰이 유효성 검증을 통과할 가능성은 매우 낮다고 할 수 있습니다.
결국 서버만 유효한 토큰을 발급하고 검증할 수 있다는 점 때문에, 서버는 사용자의 상태를 저장하지 않고 Stateless 속성을 유지할 수 있습니다.
완전한 Stateless?
그렇다면 토큰 방식 로그인을 선택했을 때 서버는 완전히 stateless 할 수 있을까요? 위의 내용을 모두 뒤집는 것 같은 말이지만, 실제로 구현하다 보면 서버가 일정 부분 사용자의 상태를 관리해야 되는 상황에 부딪히게 됩니다.
먼저 탈취된 토큰에 대응해야할 때입니다. 이를 서버에서 인지하기는 매우 어려운 일이지만, 만약 유효한 토큰이 해커에게 탈취되어 계속 사용되고 있고 이를 막아야 한다면 서버에서 특정 토큰 값을 기억하고 접근을 제한해야 합니다. 혹은 해당 토큰이 만료될 때까지 사용자의 접근을 막을 수도 있겠죠. 어느 쪽이든 서버는 사용자의 '특정 상태를 기억'해야 합니다.
위의 상황이 실제로 인지하고 대응할 가능성이 낮은 경우라면, 두번째 예시는 보다 일반적인 경우라고 할 수 있습니다. Refresh Token을 사용하여 사용자 편의성을 증대시키는 경우입니다. 토큰에 대한 정책도 다양해서 모든 경우에 적용되진 않겠지만, 일반적으로 인증에 사용되는 Access Token은 유효 기간이 짧고 재발급에 사용되는 Refresh Token은 유효기간이 상대적으로 길게 설정됩니다.
이 경우 서버가 Refresh Token 정보를 전혀 모른다면 서버가 자체적으로 사용자의 로그아웃 기능을 제공하기 어려울 수 있습니다. 사용자가 로그아웃을 요청했는데 클라이언트에서 모종의 이유로 Refresh Token을 제거하지 못했다면 사용자는 계속해서 로그인 없이 Access Token을 획득할 수 있게 되기 때문입니다. 결국 Token의 재사용을 막는 블랙리스트를 만들던가, 처음부터 유효한 Refresh Token 정보를 저장해 두었다가 로그아웃 시 제거하는 등의 방법을 선택해야 하는데 이 역시 사용자의 특정 상태를 기억하는 행위가 됩니다.
하지만, However, 그러나, Nevertheless...
그럼에도 불구하고 요즘은 Session 방식 로그인 대신 Token방식 로그인이 주로 사용되는 것으로 보입니다. 완전한 stateless는 아닐 지언정 여전히 일부 상황에선 DB나 Session에 접근하지 않고 인증을 처리할 수 있어서 서버 리소스면에서 유리한 건 여전하기 때문입니다. 결국 토큰은 계속해서 사용하게 될 것이고, 어떤 정책 / 방식으로 사용해야 안정성과 효율성을 모두 잡을 수 있을지에 대한 고민이 필요한 것 같습니다. 자주 사용하다 보면 더 좋은 방법론에 닿으리라 믿습니다.
'Backend > Spring' 카테고리의 다른 글
[예약 구매 프로젝트] Test Code (0) | 2024.07.30 |
---|---|
[예약 구매 프로젝트] 상품 조회 캐싱 추가 적용 - 성능 테스트 (0) | 2024.07.27 |
[예약 구매 프로젝트] 동시 주문 요청 처리 (0) | 2024.07.24 |
[예약 구매 프로젝트] 캐싱 전략 적용 (4) | 2024.07.23 |
[예약 구매 프로젝트]Spring Security - JWT 적용 (0) | 2024.06.26 |