웹 개발을 하다보니, 가끔 접속부터 아무 창이 뜨지 않는(내가 만든 페이지가 보여지지 않는) 상황이 발생한다. F12를 눌러 개발자 모드로 들어가서 오류 문구를 확인해보면, CORS 에러를 종종 만나곤 한다. 그럴 때는 옆자리의 백엔드 개발 담당자에게 CORS 뜬다고 "이거 왜 이래요?" 하면 해결해주기도 한다. 이참에 CORS가 뭐고, 왜 오류가 나는 것인지 찾아보도록 하겠다.

CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 보안상의 이유로 동일 출처 정책(SOP, Same-Origin Policy)을 우회할 수 있도록 허용하는 메커니즘이다. 웹 페이지에서 외부 도메인(출처)의 자원에 접근할 때, CORS를 통해 서버가 명시적으로 해당 도메인에서의 요청을 허용해야만 브라우저가 그 요청을 수행할 수 있다. 일단은 쉽게 이해하기 위해서, "브라우저에서 현재 보고 있는 화면과 무관한 서버의 파일들을 아무거나 막 열람할 수 없게 해야 하지 않을까?"라는 의문을 가져볼 수 있겠다. 그럼 일단 동일 출처 정책(Same-Origin Policy)가 무엇인지 보자.

 

동일 출처 정책(Same-Origin Policy)란?

SOP(Same-Origin Policy)는 웹 보안 모델 중 하나로, 한 출처(origin)의 웹 페이지가 다른 출처의 리소스에 임의로 접근하는 것을 방지하는 브라우저 보안 기능이다. 출처는 Scheme(프로토콜), Host(도메인), Port의 조합(우리가 흔히 아는 주소)으로 정의된다. 동일 출처 정책은 악의적인 웹사이트가 사용자의 중요한 정보를 무단으로 도용하거나 조작하는 것을 방지하기 위해 도입되었다. 예를 들어,

  • https://luceeverde.tistory.com과 https://luceeverde.tistory.com은 동일 출처
  • http://luceeverde.tistory.com과 https://luceeverde.tistory.com은 다른 출처(프로토콜 차이)
  • https://luceeverde.tistory.com과 https://tistory.com은 다른 출처(서브도메인 차이)
  • https://luceeverde.tistory.com:3000과 https://luceeverde.tistory.com:4000은 다른 출처(포트 차이)

라고 이해할 수 있겠다. 그렇다면 위에서 CORS는 SOP(동일 출처 정책)을 우회할 수 있도록 허용하는 메커니즘이라고 했으니, 왜 우회를 해야하는 상황이 생기는 걸까?

 

CORS의 필요성

SOP(동일 출처 정책)는 보안 측면에서 매우 중요하지만, 실제 애플리케이션에서는 여러 출처에 걸쳐 데이터를 공유해야 하는 경우가 많다. 예를 들어, 프론트엔드 애플리케이션과 API 서버가 다른 도메인에서 실행되는 경우, 웹 페이지는 API 서버에 요청을 보내 자원을 가져와야 한다. 이때, 동일 출처 정책에 의해 차단될 수 있는데, CORS는 이를 우회하는 방법을 제공한다.

 

CORS의 작동 방식

CORS는 서버가 클라이언트로부터 요청을 받을 때, 해당 요청을 허용할지 결정하는 방식으로 동작한다. 서버는 브라우저가 요청한 출처(origin)에 대해 응답 헤더를 통해 요청을 허용할지 명시적으로 지정해야 한다.

  • 프리플라이트 요청(Preflight Request): 클라이언트가 서버로 특정 조건에 해당하는 요청을 보내기 전에 OPTIONS 메서드를 사용하여 사전 요청(프리플라이트 요청)을 한다. 이를 통해 서버가 해당 요청을 허용하는지 확인한다. 프리플라이트 요청 예시와 응답 예시를 한번 보자.
  • 실제 요청: 프리플라이트 요청이 성공하면, 클라이언트는 실제 요청을 서버로 전송할 수 있다.
/*
프리플라이트 요청 예시
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: http://mydomain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

프리플라이트 응답 예시
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://mydomain.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type


프리플라이트 요청 성공 이후


실제 요청 예시
POST /api/data HTTP/1.1
Host: api.example.com
Origin: http://mydomain.com
Content-Type: application/json

실제 응답 예시
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://mydomain.com
*/

그런데 이러한 CORS 관련 문제는 프론트 개발을 하는 내가 뭔가 작업을 진행해서 문제를 고치는 영역이 아닌 걸 보면, 서버에서 어떠한 처리를 통해 문제를 해결했다는 것인데, (실제로 프리플라이트 요청을 내가 만들어서 서버로 쏘도록 한 것은 아닌데 CORS 오류 문제가 해결되었다.)

 

CORS 응답 헤더

CORS 사용 예시 (서버 측 설정)

CORS 관련 문제와 해결 방법

마무리

 

 

 

728x90

어떤 서비스를 만들던, 로그인은 빠질 수 없는 필수 기능이 아닐까 싶다. 그리고 개발 초기에 구현하는 기능이기 때문에 웹/앱 관계없이 개발자라면 꼭 알아두어야 하는 기능이 아닐까 싶다. JWT(Json Web Token) 기반의 로그인 인증 방식에 대해 나름 공부하며 얻은 지식들을 정리해본다.

JWT 기반의 로그인 인증 방식이 널리 사용되는 이유 중 하나는 Stateless(무상태성 이라고도 하는 듯)라는 특성 때문이다. 그럼 Stateless는 뭐고, 왜 좋은걸까? 그걸 알기 위해선 Stateful한 방식(세션 기반 인증)과 먼저 비교하여 이해할 필요가 있다.

 

Stateful한 세션 기반 인증 방식과 장단점

Stateful한 세션 기반 인증 방식은 전통적인 웹 애플리케이션에서 많이 사용되는 인증 방식이다. Stateful이란, 서버가 클라이언트와의 상호작용에서 상태(state)를 유지한다는 의미이다. 서버는 클라이언트와의 각 요청을 처리하면서 이전 요청에서 생성된 정보를 저장하고 관리한다. 이 정보는 보통 세션(Session)이라고 불리며, 서버는 클라이언트의 세션을 유지함으로써, 사용자가 로그인한 후에 사용자를 인식할 수 있게 된다. 세션 기반 인증은 대략 다음과 같은 절차로 작동한다.

  1. 로그인 요청 : 클라이언트가 서버에 로그인 요청을 보냄. 이 요청에는 사용자의 아이디와 패스워드와 같은 자격 증명이 포함된다.
  2. 서버에서 인증 : 서버는 이 자격 증명을 받아서 DB를 조회해 유효성을 확인한다. 유효하면 클라이언트에 대해 세션을 생성한다. 이 세션은 서버 메모리나 데이터베이스에 저장된다. 세션에는 고유한 식별자(Session ID)가 부여된다. 내가 처음 세션 개념을 알게 되었을 때에는 그냥 뭉뚱그려 개별 유저에게 부여되는 딕셔너리 형식의 자료구조라고 이해하고 사용했다.
  3. 세션ID 전달 : 서버는 이 세션 ID를 클라이언트에게 쿠키에 담아 전달한다. 이 쿠키는 클라이언트가 서버에 요청을 보낼 때마다 자동으로 포함된다.
  4. 요청 처리 : 클라이언트가 서버에 요청을 보낼 때마다, 클라이언트의 브라우저는 쿠키에 포함된 세션 ID를 서버에 보낸다. 서버는 이 세션 ID를 확인하여, 해당 사용자가 이미 인증된 상태인지, 어떤 사용자인지 등을 파악한다.
  5. 세션 유지 : 서버는 사용자가 로그아웃하거나 세션이 만료되기 전까지, 해당 세션 ID를 유지하며 클라이언트의 상태를 관리한다.

세션 기반 인증 방식을 쇼핑몰 웹사이트를 예로 들어 생각해보면, 사용자가 쇼핑몰 웹사이트에 로그인하면 서버는 사용자의 정보를 인증하고, 이 사용자에게 고유한 세션을 생성한다. 세션 ID를 'ABCD1234'라고 가정해보겠다. 서버는 이 'ABCD1234'라는 세션 ID를 서버 메모리나 DB에 저장하고, 이 세션에 사용자의 정보와 상태(ex: 장바구니에 담긴 상품 리스트)를 저장한다. 서버는 'ABCD1234'라고 하는 세션 ID를 사용자의 브라우저로 쿠키에 담아 보낸다. 사용자가 상품을 장바구니에 추가하면, 서버는 이 요청을 받아 'ABCD1234'라는 세션에 해당 상품 정보를 추가한다. 사용자가 로그아웃하면, 서버는 'ABCD1234' 세션을 삭제하고 더 이상 해당 세션을 유지하지 않는다. 이후의 요청에서는 더이상 해당 사용자를 인증된 상태로 인식하지 않게 된다.

Stateful한 세션 기반 인증의 장점은 사용자별 상태를 별도로 계속 유지하기 때문에 사용자의 특정 상태(ex: 장바구니 리스트, 사용자 설정 등)를 쉽게 관리할 수 있다. 또한, 세션 타임아웃, 세션 무효화 등의 관리 기능을 통해 사용자의 인증 상태를 관리할 수 있다.

Stateful한 세션 기반 인증의 단점으로는 서버가 각 클라이언트마다 세션을 유지해야 하므로, 사용자가 많아질수록 서버 메모리나 DB의 부하가 증가한다. 특히, 여러 서버로 확장(scale-out)할 때, 세션을 공유하는 것이 복잡해질 수 있다. 그리고 사용자의 세션이 특정 서버에 종속되기 때문에, 서버 간의 부하 분산이나 장애 처리에 어려움이 생길 수 있다.

 

Stateless한 JWT 기반 인증 방식과 장단점

그럼 이 글의 핵심인 Stateless한 방식에 대해 알아보겠다. 단어 의미대로, 서버가 클라이언트와의 각 요청 사이에 어떠한 상태도 유지하지 않는다는 의미이다. 서버는 각 요청을 독립적으로 처리하며, 이전 요청이나 이후 요청과의 연관성을 고려하지 않는다. 조금 더 자세히 들어가면, 클라이언트가 로그인하면, 서버는 JWT를 생성하여 클라이언트에게 전달한다. 이 JWT는 클라이언트가 이후 요청을 할 때마다 포함시켜 보내며, 서버는 이 토큰을 검증하여 클라이언트를 인증한다. 모든 필요한 정보는 JWT 자체에 포함되어 있기 때문에 세션이나 상태 정보를 유지할 필요가 없다.

Stateless의 장점으로는 서버를 수평적으로 쉽게 확장할 수 있다는 점이다. 서버 부하가 증가할 때 새로운 서버 인스턴스를 추가하여 트래픽을 분산시키는 것이 쉽고, 서버 코드와 인프라가 단순해질 수 있다. 또한, 클라이언트의 상태를 신경 쓰지 않고 요청을 처리할 수 있어 서비스 가동 중단 없이 쉽게 서버를 재시작하거나 배포할 수 있다는 것을 의미한다.

Stateless의 단점으로는 JWT를 클라이언트가 보관하고, 요청마다 전송하기 때문에, 만료된 토큰을 강제로 무효화하기 어렵다. 이를 해결하기 위해서 짧은 만료주기의 access token과 상대적으로 긴 만료주기의 refresh token을 같이 사용하는 방식 등으로 많이 운영된다. 또한, JWT에는 사용자에 대한 정보를 인코딩하여 저장하기 때문에, 세션 ID만 저장하는 방식보다 더 큰 크기의 데이터를 전송해야 한다. 이로 인해 네트워크 대역폭이 더 많이 사용될 수 있다.

728x90

+ Recent posts