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

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

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

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 응답에서 사용되는 주요 상태 코드들만 다루었다

 

 

 

쿠키

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

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

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