JUINTINATION
REST API 인증 기법 본문
지난 해커톤 이후 Spring Security 관련 글들(Board Clone 프로젝트에 Spring Security를 활용한 로그인 기능 구현하기, Spring Security RoleHierarchy로 계층권한 설정하기)을 작성하다가 번아웃이 너무 심하게 왔다는 핑계로 너무 오랫동안 아무런 글도 안 쓰고 쉬던 중에 예전에 주문했던 책(React.js, 스프링 부트, AWS로 배우는 웹 개발 101)이 도착했다. 곧 개강이라 학술제 준비도 하고, 졸업 준비도 해야 해서 바빠질 것 같다는 생각에 키보드 앞에 앉았다. 이 책은 사실 AWS에 대해 공부하려고 주문했던 책인데 스프링 시큐리티 관련 좋은 글도 있어서 먼저 정리해 보려고 한다.
REST API
먼저 이전에 다른 글에서도 설명없이 사용되었고, 많이들 이미 알겠지만 REST API에 대해 먼저 정리하고자 한다.
REST는 Representational State Transfer의 약자로, 아키텍처 스타일(반복되는 아키텍처 디자인)이다. REST 아키텍처 스타일은 6가지 제약 조건으로 구성된다.
- 클라이언트-서버(Client-Server)
- 다수의 클라이언트(브라우저)가 리소스를 소비하기 위해 네트워크를 통해 리소스를 관리하는 서버에 접근하는 구조이다.
- 여기서 리소스는 REST API가 리턴할 수 있는 모든 것을 의미하며, HTML, JSON, 이미지 등의 예시가 있다.
- 상태가 없는(Stateless)
- 상태가 없다는 것은 클라이언트가 서버에 요청을 보낼 때, 이전 요청의 영향을 받지 않음을 의미한다.
- 서버가 클라이언트의 login 여부를 알고 있어야 한다면, 이는 상태가 있는(Stateful) 아키텍처이다.
- Stateless 서버는 클라이언트가 요청을 날릴 때마다 해당 요청에 리소스를 받기 위한 정보를 포함해야 한다.
- HTTP는 기본적으로 상태가 없는 프로토콜이므로, HTTP를 사용하는 웹 애플리케이션은 기본적으로 상태가 없는 구조를 따른다.
- 캐시 가능한 데이터(Cacheable)
- 서버에서 리소스를 리턴할 때 캐시(Cache)가 가능한지 여부를 명시할 수 있어야 한다.
- HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부를 명시할 수 있다.
- 일관적인 인터페이스(Uniform Interface)
- 리소스에 접근하는 방식, 요청의 형식, 응답의 형식이 애플리케이션 전반에 걸쳐 URI, 요청의 형태와 응답의 형태가 일관적이어야 한다.
- 예를 들어 {서버 주소}/todo/는 JSON 형식의 리소스를 리턴했는데 {서버 주소}/account/는 HTML를 리턴한다면, 이런 인터페이스는 리턴 타입에 일관성이 있다고 할 수 없다.
- 레이어 시스템(Layered System)
- 클라이언트가 서버에 요청을 날릴 때, 여러 개의 레이어로 된 서버를 거칠 수 있다.
- 예를 들어 서버는 인증 서버, 캐싱 서버, 로드 밸런서를 거쳐서 최종적으로 애플리케이션에 도착한다고 할 때, 이 사이의 레이어들은 요청과 응답에 어떤 영향도 않으며, 클라이언트는 서버의 레이어 존재 유무를 알지 못한다.
- 코드 온-디맨드(Code-On-Demand)
- 클라이언트는 서버에 코드를 요청할 수 있고, 서버가 리턴한 코드를 실행할 수 있으며, 이 제약은 선택사항이다.
이 가이드라인을 따르는 API를 RESTful API라고 한다.
REST API 인증
인증(authentication)은 쉽게 얘기하면 당신이 누구냐에 대한 것이다. 실세계에 비유해보면 서비스는 내 집에 들어온 손님이 할 수 있는 활동이고, 유저인 당신은 손님이다. 당신이 와서 벨을 누르는 행위를 로그인 요청으로 비유하면, 나는 당신이 내가 아는 안전한 사람임을 확인하면 내 집으로 들어올 수 있게 해준다.
비슷한 말로 인가(authorization)가 있는데, 인가는 당신이 내 집에서 할 수 있는 것들, 즉 사용할 수 있는 자원을 정의한다. 인증된 사용자가 어떤 기능을 사용할 수 있다면, 이는 해당 자원에 인가(authorized)된 사용자인 것이다.
인증과 인가의 구현은 아키텍처 디자인과 밀접한 관계를 갖는다. 작성중인 서비스의 스케일이 쉽더라도 인증과 인가의 스케일이 어렵다면, 해당 서비스는 인증과 인가 서비스 스케일에 제약을 받게 된다. 이제부터 가장 기본적인 인증 스탠다드인 Basic 인증, 토큰 기반의 인증, 이 기존 인증 방식의 스케일적 한계와 JSON Web Token을 이용한 해결 방안을 간단하게 알아볼 것이다.
Basic 인증
우리가 종합설계 프로젝트로 구현할 애플리케이션은 위에서 언급한 Stateless 관련 이유 때문에 REST 아키텍처를 사용한다. 로그인을 제외하면 특별히 상태를 유지해야 할 이유가 없으며, HTTP를 사용하는 웹 애플리케이션을 작성할 것이기 때문에 클라이언트는 모든 요청에 리소스를 받기 위한 정보를 포함해야 한다.
그 중 모든 요청에 아이디와 비밀번호를 같이 보내는 방법을 Basic 인증이라고 한다. Basic 인증에서는 최초 로그인 후 HTTP 요청 헤더의 Authorization: 부분에 다음과 같이 아이디와 비밀번호를 콜론(:)으로 이어 붙인 후 Base64로 인코딩한 문자열을 함께 보낸다.
Authorization: Basic aGVsbG93b3JsZEBnbWFpbC5jb206MTIzNA==
이 HTTP 요청을 수신한 서버는 해당 문자열을 디코딩해 아이디와 비밀번호를 찾아낸 후, 유저 정보가 저장된 DB 또는 인증 서버의 레코드와 비교하여 일치하면 요청 받은 일을 수행한다.
하지만 이 솔루션은 아이디와 비밀번호를 노출한다는 문제점이 있다. 누군가 HTTP 요청을 가로채 문자열을 디코딩하면 아이디와 비밀번호를 모두 알아낼 수 있다.(이렇게 가로채는 것을 MITM(Manipulator in the Middle Attack)이라고 한다.)
실제로 구글 검색을 하면 맨 위에 나오는 무료로 사용 가능한 Base64 디코더로 위의 내용을 디코딩한 결과는 다음과 같다.
helloworld@gmail.com:1234
또한 이 솔루션을 이용하면 모든 요청이 일종의 로그인 요청이기 때문에 유저를 로그아웃시킬 수 없으며, 인증 서버와 유저 정보가 저장된 DB에 과부하가 걸릴 확률이 높다. 이로 인해 인증 서버가 단일 장애점(전체 시스템을 가동불가하게 만드는 시스템의 한 부분)이 될 수 있다는 문제점 등에 의해 이 솔루션은 서비스가 커지고, 인증 서버에 요청해야 하는 일이 많아지는 경우에 적합하지 않다.
토큰 기반 인증
토큰(Token)은 사용자를 구별할 수 있는 문자열로 최초 로그인 시 서버가 만들어주며, 클라이언트는 이후 요청에 아이디와 비밀번호 대신 넘겨 자신이 인증된 사용자임을 알리는 일종의 출입증이다. 토큰을 기반으로 하는 요청은 아래와 같이 HTTP 요청 헤더의 Authorization: 부분에 Bearer <TOKEN>을 명시한다.
Authorization: Bearer Nn4d1MOVLZg79s...(이후 생략)
이 솔루션은 Basic 인증을 이용한 로그인과 달리 아이디와 비밀번호를 매번 네트워크를 통해 전송해야 할 필요가 없으므로 보안 측면에서 조금 더 안전하다. 또한 토큰은 서버가 마음대로 생성할 수 있으므로 사용자의 인가 정보, 유효기간 등을 정해 관리할 수 있으며, 디바이스마다 다른 토큰을 생성해주고, 다른 유효기간을 정하거나 임의로 로그아웃할 수도 있다.
하지만 이 디자인은 Basic 인증에서 마주한 스케일 문제를 해결하지 않는다. 또한 토큰은 이름만 바뀐 세션이라는 생각이 들 수도 있는데, 이는 실제로 기능적으로는 거의 같은 기능을 하며, 제약 또한 비슷하다. 결국 토큰을 이용하는 것만으로는 스케일 문제를 해결할 수 없다.
JSON 웹 토큰
서버에 의해 전자 서명된 토큰을 이용하면 인증으로 인한 스케일 문제를 해결할 수 있다. 이러한 전자 서명된 토큰 중 하나가 바로 JSON 웹 토큰, 이하 JWT이다. JWT는 오픈 스탠다드로, 아래와 같이 {header}.{payload}.{signature}로 구성되어 있다.
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiVVNFUiIsInNvY2lhbCI6ZmFsc2UsIm5pY2tuYW1lIjoiU2FtcGxlVXNlciIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsIm1ubyI6MywiaWF0IjoxNzIzNDYzMjg5LCJleHAiOjE3MjM0NjM4ODl9.RIvyWuVAfuFy9EyoUtA5hyuHN-h5vM2RxHreWu7QQsY
실제로 내가 작성중인 membership-api-server에서 로그인이 성공했을 때 발행되는 accessToken의 예시로, 책의 예시를 그대로 작성하기 귀찮아서 가져왔다. https://jwt.io/ 에서 secret key와 함께 확인했을 때 위와 같이 header, payload, signature를 확인할 수 있으며, 각 파트의 필드의 의미는 다음과 같다.
- header(헤더)
- typ: Type을 줄인 말로, 이 토큰의 타입을 의미한다.
- alg: Algorithm을 줄인 말로, 이 토큰의 발행에 사용된 해시 알고리즘의 종류를 의미한다.
- payload(내용)
- Payload 부분에는 토큰에 담을 정보가 들어있는데, name-value 쌍으로 이루어진 정보의 한 조각을 클레임(claim) 이라고 부른다. 토큰에는 여러개의 클레임을 넣을 수 있으며, 위의 예시에서 사용된 유저 관련 정보를 제외한 클레임의 예시는 다음과 같다.
- iat: issued at을 줄인 말로, 이 토큰이 발행된 날짜와 시간을 의미한다.
- exp: expiration을 줄인 말로, 이 토큰이 만료되는 시간을 의미한다.
- Payload 부분에는 토큰에 담을 정보가 들어있는데, name-value 쌍으로 이루어진 정보의 한 조각을 클레임(claim) 이라고 부른다. 토큰에는 여러개의 클레임을 넣을 수 있으며, 위의 예시에서 사용된 유저 관련 정보를 제외한 클레임의 예시는 다음과 같다.
- signature(서명)
- 토큰을 발행한 주체 Issuer가 발행한 서명으로, 토큰의 유효성 검사에 사용된다.
최초 로그인 시 서버는 사용자의 아이디와 비밀번호를 서버의 저장된 아이디와 비밀번호에 비교해 인증한다. 만약 인증된 사용자의 경우 사용자의 정보를 이용해 {헤더}.{페이로드} 부분을 작성하고, 시크릿 키로 해당 부분을 전자 서명한 뒤 전자 서명의 결과로 나온 값을 {헤더}.{페이로드}.{서명}으로 이어 붙이고, Base64로 인코딩한 후 반환한다.
이후에 누군가 이 토큰으로 리소스 접근을 요청하면, 서버는 이 토큰을 Base64로 디코딩하여 얻은 JSON을 {헤더}.{페이로드}와 {서명} 부분으로 나눈다. 서버는 {헤더}.{페이로드}와 자신이 갖고 있는 시크릿 키로 전자 서명을 만든 후, 해당 전자 서명을 HTTP 요청이 가지고 온 {서명} 부분과 비교하여 해당 토큰의 유효성을 검사한다.
서버가 방금 만든 전자 서명과 HTTP 요청의 {서명} 부분이 일치하면 토큰이 위조되지 않았다는 뜻인데, 누군가 헤더나 페이로드 부분을 변경했다면 서명이 일치하지 않기 때문이다. 따라서 인증 서버에 토큰의 유효성에 대해 물어볼 필요가 없으며, 이는 인증 서버에 부하를 일으키는 문제를 해결할 수 있다.
하지만 누군가 토큰을 훔쳐가게 된다면 당연히 해당 계정의 리소스에 접근할 수 있게 되기 때문에 Basic 인증의 아이디와 비밀번호를 노출한다는 문제점과 비슷한 문제점이 있다.
결론
Basic 인증과 Token 기반 인증, 그리고 JWT에 대해 책을 읽으며 간단하게 정리해보았다. 위에서 예시로 사용했던 토큰을 Base64 디코딩을 진행하면 아래와 같다.
{"typ":"JWT","alg":"HS256"}{"role":"USER","social":false,"nickname":"SampleUser","email":"user@example.com","mno":3,"iat":1723463289,"exp":1723463889}DZ@~rLR9+7y͑zZB
맨 끝에 전자 서명 관련 부분은 SECRET_KEY가 있으면 디코딩이 가능할 것이다. 이렇게 잘 알지도 모르고 membership-api-server에서 JWT를 사용한 로그인을 구현했는데, 직접 테스트해보며 책에 있는 내용을 이해해가는 기분이 너무 좋은 것 같다.
관련 내용을 좀 더 보충해서 더 완성도 높은 프로젝트를 작성해 봐야겠다는 생각이 들었고, 너무 오랜만에 쓰는 글이라 가독성이 안 좋게 느껴지는데 번아웃에서 벗어나려고 노력했다는 점에서 위안을 삼고 앞으로 더 열심히 살아야겠다.
'StudyNote' 카테고리의 다른 글
어댑터(Adapter) 패턴 (1) | 2024.11.02 |
---|---|
퍼사드(Facade) 패턴 (0) | 2024.11.02 |
2024 SW 융합클러스터 2.0 세종 DX 해커톤 후기 (0) | 2024.08.04 |
Postman을 활용한 API 문서 만들기 (2) | 2024.07.07 |
Swagger를 활용한 API Specification (2) | 2024.07.01 |