서버 배포 후 클라이언트의 로컬 환경에서 세션을 이용한 로그인 기능이 제대로 동작하지 않는 문제가 발생했다

확인해 보니, 서버에 세션 쿠키가 저장되지 않는 것 같았다

이 문제를 해결하기 위해 쿠키의 속성에 대해 더 자세히 알아보고 설정을 수정하게 되었다

Cookie

쿠키는 웹 애플리케이션에서 중요한 정보를 저장하고 전송할 때 사용되며, 이를 안전하게 다루기 위해 다양한 속성들을 설정할 수 있다

Secure

Secure 속성은 HTTPS 통신에서만 쿠키에 접근할 수 있도록 제한한다

이를 통해 악의적인 네트워크 공격(예: 중간자 공격)을 방지할 수 있다

(localhost 환경에서는 Secure 속성을 지정해도 HTTP로 통신이 가능하지만, 실제 배포 환경에서는 HTTPS를 사용해야한다)

SameSite

SameSite 속성은 크로스 사이트 요청에서 쿠키 전송을 제어하는 역할을 한다

이를 통해 사이트 외부에서 발생한 요청에 대한 쿠키 전송을 제한함으로써, CSRF(Cross-Site Request Forgery) 공격을 방지할 수 있다

Strict

Strict로 설정된 쿠키는 해당 도메인 내에서 발생한 요청(퍼스트 파티 쿠키)만 전송된다

즉, 외부에서 오는 모든 크로스 사이트 요청에서는 쿠키가 전송되지 않는다

예를 들어, 외부 링크나 다른 도메인에서의 접근을 통해 사용자가 사이트로 돌아올 때 쿠키가 전송되지 않는다

Lax

Lax 모드는 기본적으로 Strict와 비슷하지만, 일부 안전한 요청 (GET 메서드를 통한 외부 링크 클릭 등)에서는 쿠키 전송이 허용된다

예를 들어, 사용자가 다른 사이트에서 내 사이트로 링크를 클릭할 때는 쿠키가 전송된다

None

None으로 설정하면 동일 사이트와 크로스 사이트 모두에서 쿠키 전송이 가능하다

즉, 퍼스트 파티 쿠키와 서드 파티 쿠키가 모두 전송될 수 있어, CSRF 공격에 취약하다

크롬 80 버전 이후부터는 SameSite=None으로 설정된 쿠키는 반드시 Secure 속성과 함께 사용해야 하며, 그렇지 않으면 브라우저에서 쿠키를 차단한다

HttpOnly

HttpOnly 속성은 클라이언트의 자바스크립트로 쿠키에 접근할 수 없도록 막아준다

이를 통해 XSS(Cross-Site Scripting) 공격으로부터 쿠키 탈취를 방지할 수 있다

브라우저에서만 쿠키를 처리할 수 있도록 하여, 클라이언트 측 코드가 쿠키를 조작하지 못하게 하는 것이다

 

Hyper Text Transfer Protocol HTTP

HTTP 상태 코드(HTTP Status Codes)는 클라이언트와 서버 간의 통신 상태를 나타내는 3자리 숫자로 구성된 코드로, 클라이언트가 다음에 취해야 할 동작을 결정하는 중요한 역할을 한다

상태 코드는 1XX부터 5XX까지 다양한 범주로 나뉘며, 각각 클라이언트와 서버 간의 통신 과정에서 발생하는 특정 상황을 나타낸다

1XX: 정보 응답

  • 100 (계속): 클라이언트가 요청의 나머지 부분을 계속 전송해야 함을 의미
  • 101 (프로토콜 전환): 서버가 클라이언트의 프로토콜 전환 요청을 승인
  • 102 (처리 중): 요청을 처리 중이며 응답이 지연됨을 알림

2XX: 성공 응답

  • 200 (성공): 요청이 성공적으로 처리됨
  • 201 (작성됨): 요청이 성공적으로 처리되었고, 서버가 새로운 리소스를 생성함 ex) 회원 가입 후 새로운 사용자 계정 생성
  • 202 (허용됨): 요청을 접수했지만 아직 처리되지 않음
  • 204 (콘텐츠 없음): 요청이 성공했으나 응답 본문이 없음 ex) 데이터 삭제 요청 후 별도의 응답 데이터가 필요 없을 때

3XX: 리다이렉션 응답

  • 301 (영구 이동): 요청한 리소스가 영구적으로 새로운 URL로 이동됨
  • 302 (임시 이동): 요청한 리소스가 일시적으로 다른 URL에서 제공됨 ex) 사용자를 다른 페이지로 임시 리다이렉트할 때
  • 304 (수정되지 않음): 리소스가 변경되지 않았으며, 캐시된 버전을 사용 가능

4XX: 클라이언트 오류

  • 400 (잘못된 요청): 서버가 요청을 이해할 수 없거나 잘못된 데이터를 받았을 때 사용
  • 401 (권한 없음): 인증이 필요하지만 제공되지 않았거나 실패한 경우 ex) 로그인 없이 보호된 리소스에 접근 시도
  • 403 (금지됨): 서버가 요청을 거부, 인증은 성공했으나, 클라이언트에게 해당 리소스에 대한 접근 권한이 없는 경우
  • 404 (찾을 수 없음): 요청한 리소스를 찾을 수 없음 ex) 잘못된 URL 요청

5XX: 서버 오류

  • 500 (내부 서버 오류): 서버에서 처리 중 예기치 않은 오류 발생
  • 502 (잘못된 게이트웨이): 게이트웨이 또는 프록시 서버가 상위 서버로부터 잘못된 응답을 받음
  • 503 (서비스 이용 불가): 서버가 일시적으로 요청을 처리할 수 없음 (과부하 상태이거나 유지보수 중일 때)

보통 API 응답에서 사용되는 주요 상태 코드들만 다루었다

배포를 위해 SSL인증과 DNS설정을 하고 있었다

A, CNAME 레코드를 추가하라는데

도통 이게 무슨 소린지 모르는채로 넘어갈 수가 없어 알아보았다

DNS가 뭐임?

인터넷에 연결된 컴퓨터 한대를 host 호스트라고 한다

호스트와 호스트가 통신하기 위해서는 주소가 필요하고, 이를 위해 IP 주소를 사용한다

하지만 매번 숫자로 된 IP주소를 외우기엔 불편해 만들어진 것이 도메인 네임 시스템 DNS이다

 

DNS의 핵심은 DNS Server인데, 수많은 도메인 이름과 그에 대응하는 IP 주소가 저장된 분산된 데이터베이스이다

우리 컴퓨터에서 www.어떤도메인.org 를 요청할 때, 운영체제는 DNS 서버에서 www.어떤도메인.org 의 ip 주소를 요청하여 받아온다

그 ip 주소를 이용해 우리가 원하는 도메인으로 접속하게 된다

hosts 파일

사실 DNS 서버를 거치지 않고도 hosts 파일을 통해 자주 가는 사이트의 IP 주소를 저장해둘수도 있다 (전화번호부처럼)

그러나 hosts 파일을 해킹 당해 수정되버리면 피싱 사이트로 유도될 위험이 있기 때문에 보안에 매우 유의해야 한다

public DNS, ISP

그렇다면 우리 컴퓨터는 DNS 서버의 위치를 어떻게 알고 요청을 하는가?

그건 바로 우리가 가입한 통신사 (KT, SK, LG U+)에서 기본적으로 제공하는 DNS 서버를 사용하기 때문이다

이런 통신사를 ISP Internet Service Provider 라고 한다

그런데 한가지 상상의 나래를 펼치자면 웹 서핑을 하려면 ISP에서 제공하는 DNS 서버를 사용해야하고, 그렇다면 ISP에서 내가 접속한 사이트를 다 알수도 있지 않을까?!

아무튼 모종의 이유로 통신사에서 제공하는 DNS server를 이용하고 싶지 않을 땐, public DNS를 사용해보자

구글이 만든 8.8.8.8 public DNS도 있고, 1.1.1.1로 쌈박한 주소를 가지고 있는 Cloudflare의 public DNS도 있다 (1.1.1.1은 자기네가 구글이나 보통의 ISP보다 속도도 빠르다고 어필한다)

도메인 이름의 구조

도메인의 끝에는 사실 . 이 생략되어 있다

점.은 루트를 의미하고 그 앞으로 탑 레벨 Top Level, 세컨드 레벨 Second Level, 서브 레벨 sub Level 로 이루어져있다

그리고 각각의 레벨마다 해당 도메인 이름에 대응하는 IP 주소를 저장한 DNS 서버가 존재한다

루트 DNS 서버는 Top-level DNS 서버의 목록을 알고있고, Top-level DNS 는 Second-level DNS의 목록을 알고있고, 각 하위 계층의 목록들만 알고 있다
결국 서브 도메인에 접속하려는 사이트의 IP주소가 있는 것이다

 

우리의 컴퓨터는 수 많은 서브 DNS 서버의 주소를 저장하기 어렵기에 루트 DNS 서버의 주소만 알고있다

 

그래서 루트 계층에서 탑으로, 탑에서 세컨으로, 세컨에서 서브로 순차적으로 각 레벨의 DNS 서버에 접근하여 최종적으로 해당 IP 주소를 얻어내는 방식으로 통신한다

도메인 이름 등록 과정과 원리

전세계 Root name server는 ICANN 이라는 비영리 단체에서 관리한다

Top-level domain 들은 Registry 등록소라는 기관에서 담당한다

 

도메인을 등록하려면 탑레벨 도메인에 주소를 직접 등록 할 수 없고, 네임 서버 형식으로 등록해야 하므로 네임서버가 하나 필요하다

 

등록대행자를 통해 네임서버를 탑레벨 도메인에게 전달해 네임서버를 등록하고, 네임서버에 내 도메인의 IP주소를 등록하면 각각의 서버가 하위 서버를 알고 있기때문에 이제 루트 서버부터 내 IP주소를 찾아 접속이 가능해진다

 

위의 서버 이미지 밑에 각각 저장된 도메인 정보들이 있는데, 이렇게 서버에 저장되어 있는 데이터 형식을 레코드 타입이라고 한다

여기서 A 와 NS는 최종 IP주소 = Address를 뜻하는 A레코드 와 네임서버를 뜻하는 NS레코드를 뜻한다

CNAME은 또 뭔데?

A 레코드는 도메인 이름을 정확한 IP 주소에 매핑한다

CNAME 레코드는 도메인 이름을 다른 도메인 이름에 매핑하여, 간접적으로 IP 주소에 연결한다

특정 도메인이 자주 변경될 때 유용하다

예를 들어, A 레코드를 직접 수정하지 않고도 CNAME 레코드를 수정함으로써 도메인의 별칭을 통해 IP 주소가 변경되더라도 서비스 접속에 문제가 생기지 않도록 할 수 있다

즉, A 레코드가 직접 IP 주소를 가리킨다면, CNAME 레코드는 별칭처럼 다른 도메인을 가리키는 방식이다

Ref : https://youtube.com/playlist?list=PLuHgQVnccGMCI75J-rC8yZSVGZq3gYsFp&si=4ve-6cyJ3wZyV7Qu

'Backend Basics > Cloud & Deployment' 카테고리의 다른 글

로드 밸런서가 뭐임?  (0) 2024.10.08

 

 

로드 밸런서와 로드 밸런싱

서버와 클라이언트를 각각 Express와 React(TypeScript)로 나누어 개발했다

배포할 때 AWS Elastic Beanstalk을 사용하면서 로드 밸런서라는 개념을 처음 접했다

백엔드 지식이 부족해서 하나하나 설정하는 과정이 생소했는데, 정보처리기사 시험을 준비하면서 로드 밸런싱 개념이 다시 등장해 글로 정리해본다

로드 밸런싱 (Load Balancing)이란?

로드 밸런싱은 네트워크 트래픽을 여러 서버에 분배해, 한 서버에 과도한 부하가 걸리지 않도록 해주는 기술이다 이렇게 트래픽을 분산하면 특정 서버가 과부하로 인해 다운되는 상황을 방지할 수 있다

또한, 로드 밸런싱 덕분에 서버를 쉽게 추가하거나 제거할 수 있고, 트래픽의 변화에 유연하게 대응할 수 있어 확장성 측면에서 매우 유리하다

로드 밸런서 (Load Balancer)의 역할

  • 여러 대의 서버가 있을 경우, 이들 사이에 네트워크 요청을 효율적으로 분배하는 장치

만약 서버 중 하나가 다운되거나 문제가 발생하면, 로드 밸런서는 자동으로 트래픽을 다른 정상적인 서버로 리다이렉트해서 서비스의 지속성을 보장한다

로드 밸런싱의 분배 방식

  • Random (랜덤 분배): 요청을 무작위로 서버에 할당
  • Least Loaded (가장 적은 부하 서버 선택): 현재 가장 적은 양의 작업을 처리하고 있는 서버에게 요청 할당
  • Least Connection (최소 연결 서버 선택): 현재 연결된 클라이언트 수가 가장 적은 서버에 요청 할당 서버의 성능이 비슷하게 구성돼 있을 때 가장 효과적으로 트래픽을 분산할 수 있다
  • Round Robin (라운드 로빈): 순차적으로 작업을 분배해 빠르지만, 서버의 성능 차이를 고려하지 않는다
    • Weighted Round Robin (가중치 라운드 로빈): 서버의 성능 차이를 반영해, 더 성능 좋은 서버에 더 많은 요청 할당
  • Source IP Hash (소스 IP 해시): 클라이언트의 IP 주소를 해시한 결과를 기반으로 특정 서버에 요청을 보내며, 클라이언트가 항상 같은 서버에 연결될 수 있도록 한다

Ref : https://guide-fin.ncloud-docs.com/docs/networking-loadbalancer-loadbalanceroverview

'Backend Basics > Cloud & Deployment' 카테고리의 다른 글

DNS Domain Name System 가 뭐임  (2) 2024.10.09

 

 

 

쿠키

클라이언트와 서버 간에 정보를 주고받는 수단이라고 보면 된다

사용자가 처음 웹 사이트에 접속할때 사이트로부터 쿠키를 받는다 (서버 → 클라이언트)

A 사이트에 로그인을 하면, A사이트의 DB에서 회원 정보를 확인 후 ‘회원 맞음’ 인증 정보를 담은 쿠키가 브라우저에 저장된다 (로그인 → 서버 DB 확인 → 사용자)

로그인 후 글을 쓰거나 댓글을 다는 등의 새로운 요청을 하거나 재접속하더라도 자동으로 브라우저에 저장된 쿠키가 서버로 전달되어 로그인 상태를 유지할 수있다

쿠키는 도메인에 종속 되는데, 무슨 말이냐면 A 도메인에서 발급된 쿠키는 A 도메인에만 사용할 수 있다

쿠키에 유효기간을 정해줄 수 있어, 만료된 후 자동으로 삭제할 수도 있다

예시로 은행처럼 보안상 일정시간 후에 세션이 만료되어 자동 로그아웃 되는 사이트들처럼 말이다

쿠키는 인증 뿐만 아니라 여러가지 정보를 저장할 수 있는데, 장바구니 정보나 언어 설정 같은 사용자 맞춤 데이터를 저장해두는 데도 많이 사용된다

보안 강화를 위해 HTTP-Only 속성을 부여할 수 있다 이 속성은 자바스크립트에서 쿠키에 접근하지 못하게 해 XSS 공격을 방지할 수 있다 토큰 방식에서 Access Token을 쿠키에 저장할 때 권장되는 속성이다

세션

HTTP 프로토콜은 stateless(상태를 기억하지 않는) 특성을 가진다

즉, 각 요청은 독립적으로 처리되며, 서버는 이전 요청의 상태를 기억하지 못하기 때문에

요청이 들어올때 마다 요청자가 누군지 확인해야 하므로 시간과 자원이 더 들게 되는데, 이를 보완하기 위해 세션을 사용한다

세션은 서버에서 관리하는 일시적인 저장 공간으로, 서버는 클라이언트로부터 받은 세션 ID를 기반으로 사용자 상태를 유지한다

사용자가 웹사이트를 방문하면 세션 ID가 담긴 쿠키가 브라우저에 저장되고, 이 쿠키를 통해 사용자는 이후 요청에서도 동일한 세션을 유지할 수 있다

서버는 이 세션 ID를 통해 사용자의 로그인 상태를 기억하거나 개인화된 데이터를 제공할 수 있다

다만, 세션은 주로 서버에 저장되며, 서버 리소스를 소모한

사용자가 많아지면 서버에 더 많은 리소스를 필요로 할 수 있어, 이를 보완하기 위해 Redis 같은 메모리형 데이터베이스를 많이 사용한다고 한다

토큰

토큰이란 서명 같은 것인데, 마치 서류나 카드 뒷면에 서명을 하는것만으로 어떠한 계약에 효력이 생기는 것과 같이 인증된 사용자를 식별하는 디지털 서명의 역할을 한다

세션과 달리, 토큰은 DB 필요없이 서버에서 유저를 인증하는데 필요한 정보 (이를테면 세션 ID)가 담긴 토큰을 저장해서 주면 클라이언트에서 자체적으로 저장하고 이후 페이지를 요청할때 클라이언트가 그때 받은 토큰을 HTTP 요청의 헤더에 담아 서버로 보내면, 서버는 토큰이 유효한지만 검증하면 된다

때문에 토큰 기반 인증에서는 서버가 매번 DB를 확인하지 않아도 되므로, 트래픽이 많은 애플리케이션에서 효율적으로 사용할 수 있지만, 토큰이 유효한지 확인하려면 누구나 열어 볼 수 있어야 하기 때문에 아주 중요한 정보가 있으면 위험할 수 있다

예시로는 코로나때의 QR 코드 인증이 있다

JWT

JSON Web Token

토큰 방식의 인증에서 많이 사용되는 형태이다

인코딩 또는 암호화된 3가지 데이터를 이어 붙인 것으로, 세션 ID보다 훨씬 길고 괴상한 문자들로 이루어져 있음에도 공간제약이 없다 (쿠키는 공간제약이 있다)

잘보면 xxxxxxx.zzzzzz.yyyyy이런식으로 세부분으로 나뉘는데, 각각 1. 헤더 header, 2. 페이로드 payload, 3. 서명 verify signature 순으로 구성되어 있다

  1. 헤더 (Header): 토큰의 타입(JWT)과 서명에 사용할 알고리즘 정보(예: HS256)를 담고 있다
    첫번째 헤더를 디코딩하면 두가지 정보가 담겨있는데, 
    1. 토큰의 type : 타입은 항상 JWT로 고정값이다
    2. alg : 알고리즘의 약자로 3번 서명값을 만드는데 사용될 알고리즘이 지정된다
    3. HS256 등 암호화 방식 중 하나를 지정할 수 있다
  2. 페이로드 (Payload): 토큰에 담을 실제 데이터가 포함된다.(이렇게 되면 누구든 다시 디코딩을 통해 악용이 가능하므로 첫번째와 세번째 부분이 존재한다)
    가운데 페이로드 부분을 base64로 디코딩해보면 JSON 형식으로 누가 누구에게 토큰을 발급했는지, 언제까지 유효한 토큰인지, 서비스가 사용자에게 토큰을 통해 공개하기 원하는 내용들 (사용자 ID, 권한 정보)이 있다
    토큰에 담기는 이러한 데이터를 Claim 이라고 한다
  3. 서명 (Signature): 헤더와 페이로드를 합친 후, 서버에 감춰놓은 비밀 값까지 암호화 알고리즘에 넣고 돌리면 비로소 세번째 서명 값이 된다

즉, 탈취를 당하더라도 서버에 감춰놓은 비밀값을 알지 못하므로 소용없다

다만 세션 방식처럼 모든 사용자들을 기억하지 못하기 때문에 관리의 어려움이 있어 이를 보완하기 위해 토큰 기한을 짧게하는 방법이 있다

로그인을 하면 두개의 토큰이 주어지고 하나는 유효기간이 한시간이나 몇분 이하로 짧은 access 토큰, 하나는 그에 비해 꽤 길게 보통 2주 정도로 잡혀있는 refresh 토큰을 함께 사용한다

로그인시 서버는 리프레시 토큰의 상응값을 데이터베이스에 저장한다

사용자가 엑세스 토큰이 만료되면 리프레시 토큰을 서버로 보내고, 서버는 값을 대조하여 새로운 엑세스 토큰을 발행한다

리프레시 토큰만 안전하게 관리된다면 엑세스 토큰이 만료되어도 로그인 할 필요없이 클라이언트가 서버에 매번 세션을 요청하지 않고도, 엑세스 토큰을 발급 받아 자체적으로 인증 정보를 관리할 수 있다

하지만, 그사이 액세스 토큰을 탈취당한다면 즉시 무효화시키기 어렵다는 헛점은 존재한다

차이점

  세션 JMT
관리와 보안성 서버에 저장되어 있어 사용자 관리가 용이하다
특정 사용자의 세션을 강제로 만료시키거나, 계정을 차단할 수 있다
토큰 탈취 시 토큰의 유효기간 동안 차단할 방법이 없다
보안이 취약할 수 있다
서버 서버 리소스가 많이 소모된다
요청이 있을 때마다 세션을 관리하기위해 DB나 메모리에 접근해야 한다
사용자 정보를 저장할 필요가 없다
요청마다 DB에 접근하지 않아도 된다
네트워크 트래픽을 줄일 수 있다

장단점이기도 한 차이점을 표를 정리해봤다

세션은 요청이 있을때마다 DB를 탐색해야하고 ,사람이 많으면 DB 또한 더 필요하다

(이를 보완하기 위해 빠르고 저렴한 redis 메모리형 데이터베이스 서버를 많이 쓴다고 한다)

토큰은 DB를 매번 확인하지 않아도 되며 JWT은 공간차지도 안한다

 

참고 자료

https://www.youtube.com/watch?v=tosLBcAX1vk

https://www.youtube.com/watch?v=1QiOXWEbqYQ

https://youtu.be/aU4bju5kB_Q?si=CUpPENp_Px2hMkYf

 

썸네일 글자수 제한으로 s가 빠졌다 (credentials:ture)

 

CORS 설정과 관련해 문제 해결 경험을 기록한다

하나의 프로젝트 레포 안에서 서버 폴더는 Express로 , 클라이언트 폴더는 React와 TypeScript를 사용했다

프로젝트 배포 전에는 postman으로 서버 기능을 테스트했고 npm start 명령어로 클라이언트 화면을 보면서 작업했다

그런데 문제는 클라이언트에서 서버에 요청을 보내면 서버와의 연결이 되지 않는 것이었다

원인은 CORS(Cross-Origin Resource Sharing) 설정이었다

CORS는 간단히 말하면 서로 다른 출처(예: 서로 다른 도메인, 포트) 간의 통신을 허용하거나 제한할 수 있게 해주는 보안 기능이다

기본적으로 출처가 다른 경우에는 쿠키나 인증 관련 헤더(예: Authorization 헤더)를 포함한 요청을 보낼 수 없다

때문에 클라이언트가 서버에 인증 정보를 담아 요청을 보내려면 프론트엔드와 서버 양측에서 CORS 설정을 해야한다

먼저 서버 코드에 이런식으로 추가해야 한다

app.use(
  cors({
    origin: "http://localhost:9000",
    credentials: true,
  })
);
app.use(express.json());

 

origin 옵션에는 작업중인 클라이언트의 URL을 적어주면 된다

(물론 배포후에는 배포한 클라이언트의 URL로 바꿔줘야한다)

여기서 credentials이 나오는데, credentials 옵션은 인증 정보를 포함한 요청에 관한 옵션이다

true로 설정해주면 요청을 허용해 클라이언트에서 서버로 인증 정보를 담아 요청을 보낼 수 있게된다

 

다음으로, 클라이언트 코드는 fetch API로 이렇게 수정했다

export const loginUser = async (username: string, password: string) => {
  const response = await fetch(`${baseURL}/auth/login`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ username, password }),
    credentials: "include",
  });

  if (!response.ok) {
    throw new Error("Failed to login");
  }
  return await response.json();
};

export const logoutUser = async () => {
  const response = await fetch(`${baseURL}/auth/logout`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "include",
  });

  if (!response.ok) {
    throw new Error("Failed to logout");
  }
};

 

credentials: "include",로 설정해주면 모든 요청에 인증 정보를 담아간다

이 옵션은 총 3가지 값을 사용할 수 있다

  • same-origin (기본값) : 같은 출처 간 요청에만 인증 정보를 담는다.
  • include : 모든 요청에 인증 정보를 담는다.
  • omit : 모든 요청에 인증 정보를 담지 않는다.

참조 : https://velog.io/@garcon/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-CORS%EC%99%80-credentials

+ Recent posts