시리즈
👉 CS 네트워크 스터디 1주차
👉 CS 네트워크 스터디 2주차
👉 CS 네트워크 스터디 3주차
👉 CS 네트워크 스터디 4주차
쿠키와 세션
HTTP 프로토콜은 기본적으로 상태가 없는 프로토콜입니다. 웹 서비스에서 제공하는 기능들은 개인별로 맞춤화된 서비스가 많습니다. 하지만 HTTP는 상태가 없기 때문에 방문한 개인의 정보를 기억하는 별도의 방법을 사용해야 합니다.
초기에는 이러한 제약을 해결하기 위해 클라이언트의 IP 주소를 사용한 방식을 사용하였습니다. IP 주소는 사용자가 아닌 통신하는 단말을 가리키기 때문에 개개인이라는 확신을 할 수 없고, 보안상 실제 IP 주소를 쉽게 노출시키지 않기 때문에 한계가 명확하게 존재하는 방식이었습니다. 현재는 쿠키(Cookie)와 세션(Session) 등을 활용하여 HTTP 프로토콜의 이러한 약점을 보완하고 있습니다.
쿠키
쿠키는 사용자의 브라우저에 식별 데이터를 보관하는 방식입니다. 세션 쿠키(session cookie)와 지속 쿠키(persistent cookie) 두 가지로 분류됩니다.
- 세션 쿠키: 사용자가 브라우저를 닫으면 삭제됩니다.
- 지속 쿠키: 사용자 디스크에 보관되어 브라우저를 닫거나 PC를 재시작하여도 보존됩니다.
쿠키의 동작방식
사용자가 웹 사이트에 접속하면 해당 사용자를 식별하기 위해 개별 값을 쿠키에 할당합니다. 쿠키는 임의의 key=value
형태의 리스트를 가지고 있고 HTTP 응답 헤더에 기술하여 사용자에게 전달할 수 있습니다.
지난번에 HTTP 쿠키의 개념과 활용 방법 포스팅을 통해 대략적으로 웹 서버(Express.js)에서 쿠키를 전송하는 방법에 대해 다룬적이 있습니다. Set-Cookie
필드를 통해 쿠키를 명시할 수 있습니다.
const http = require('http');
http
.createServer(async (request, response) => {
const method = request.mtehod;
const url = request.url;
if (method === 'POST' && url === '/signup') {
let body = '';
request.on('data', (data) => {
body += data;
});
return request.on('end', () => {
const result = {
result: true,
message: 'success'
};
body = JSON.parse(body);
//응답 헤더에 쿠키 설정
response.writeHead(200, {
'content-Type': 'Application/json; charset=utf-8',
'Set-Cookie': `user-id=${encodeURIComponent(body.id)}`
});
response.end(JSON.stringify(result));
});
}
})
.listen(8088, () => {
console.log('Server Start!!');
});
브라우저는 방문한 여러 사이트에서 제공하는 쿠키들을 모두 보관할 수 있지만, 모든 사이트마다 쿠키를 전송하지는 않습니다. 그 이유는 성능 저하와 각 사이트마다 서로 다른 쿠키 이름을 설정하기 때문에 효율성이 떨어지기 때문입니다.
주요 속성(Attributes)
- Domain
어떤 도메인을 가진 사이트가 쿠키를 읽는지 명시할 수 있습니다.Set-Cookie: user="user123"; domain="jinyisland.kr"
- Path
특정 경로에서 쿠키를 사용할 때 경로를 명시할 수 있습니다. 예를들어Path=/user
라면, /user 하위의 URL 요청시에만 쿠키를 사용합니다. - Expires
쿠키의 유효시간을 가리키는 날짜 문자열(요일, DD-MM-YY HH:MM:SS GMT)을 명시합니다.Expires
를 명시하지 않으면 기본적으로 쿠키는 세션 쿠키로 동작하며 브라우저가 닫힐 때 파기됩니다.Set-Cookie: expries=Wed, 21 Oct 2015 07:28:00 GMT
- Secure
HTTP가 SSL 보안 연결을 사용하는 경우(HTTPS)에만 쿠키를 전송하도록 명시합니다.Set-Cookie: private_key=123; secure
- HttpOnly
사용자측에서 스크립트를 통한 쿠키 접근을 방지합니다.
Set-Cookie: private_key=123; secure; HttpOnly;
세션
출처: Microsoft Ignite - HTTP Cookie (Session ID)
세션도 본질적으로 쿠키를 활용해서 식별할 수 있는 데이터를 전송하는 방법이지만 쿠키와 다르게 데이터를 서버에 저장하고, 클라이언트에게 세션을 조회할 수 있는 세션 ID을 전송한 뒤 클라이안트가 요청한 session-id
를대조하여 서버에 저장되어 있는 세션 데이터를 응답해주는 방식입니다.
웹 서버에 저장하기 때문에 쿠키에 비해 상대적으로 안전하고 서버 용량에 따라 유동적으로 크기를 설정할 수 있다는 장점이 있습니다. 하지만 별도로 클라이언트에서 전송한 세션 ID를 검증하는 프로세스가 필요하기 때문에 성능은 쿠키와 비교했을 때 상대적으로 느릴 수 있습니다.
네트워크 보안
SOP(Same-Origin Policy)
직역하자면 동일 출처 정책이라는 의미로, 다른 출처(호스트, 스킴, 도메인에 따라 분류)에서 가져온 리소스와의 상호 작용을 제한하는 것을 의미합니다. 즉 리소스를 요청할 때에는 동일한 호스트, 스킴, 도메인 서버내에서만 요청이 가능합니다.
https://jinyisland.kr/post/express-start/
https://jinyisland.kr/post/hugo-ga/
예를들어 위 두 URL은 다르지만 스킴(https), 도메인(jinyisland.kr)이 같기 때문에 리소스를 서로 요청할 수 있습니다. 만약 한 곳에서 http
프로토콜을 사용하는 경우에는 SOP 정책에 따라 리소스 요청을 불허합니다. SOP 정책이 등장한 배경에는 신뢰되지 않은 다른 출처로부터 악의적인 공격을 방어하기 위함이었습니다.
하지만 현재 많은 웹 서비스에서는 여러 개의 API 서버를 분리하는 경우도 있으며 외부 API와 협력하여 작업하는 경우도 있습니다. 이를 통해 선택적으로 CORS 정책을 통해 외부 출처로부터 리소스를 요청할 수 있습니다.
CORS (Cross-Origin Resource Sharing)
앞서 말씀드린대로 외부 도메인으로부터 리소스를 상호 작용할 수 있도록 선택적으로 허용하는 정책 입니다. 이를직역하면 교차 출처 리소스 공유라고 합니다. CORS는 세 가지 경우에 따라 동작하는 방식을 설명드릴 수 있습니다.
단순 요청(Simple Requests)
HTTP에서 단순 요청이란 다음과 같은 상황을 만족하는 경우 입니다.
- GET, POST, HAED 요청
- User-Agent가 자동으로 설정하지 않는 수동으로 설정한 헤더 (Accept, Accept-Language, Content-Type, Content-Language 등)
Content-Type
경우 다음의 값들이 포함되는 경우 (application/x-www-form-urlencoded, multipart/form-data, text/plain)
단순 요청 상황에서는 서버에서 설정한 Access-Control-Allow-Origin
값이 와일드카드(*)이거나 요청한 도메인과 맞는 경우에만 리소스 요청을 허용합니다.
프리플라이트 요청 (Preflighted Request)
단순 요청이 아닌 상황에서는 HTTP OPTIONS 메서드를 통해 외부 출처에게 요청을 보내 전송하기 안전한지 미리 확인하는 방식으로 외부 리소스와 상호작용을 수행합니다.
#Request
OPTIONS /resources/post-here/ HTTP/1.1
...
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
#Response
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method
헤더는 실제 요청을 전송할 때 POST
로 전송하는 것임을 알려줍니다. (Preflighted 요청에서만 사용) 만약 응답 헤더에 요청한 메서드가 명시되어 있다면, 이는 안전한 출처로 간주되며 이후에 리소스 요청을 수행할 수 있습니다.
자격 증명 요청(With Credentialed)
자격 증명(credentials) 요청은 쿠키를 담아서 보내는 요청이거나, 인증을 위해 헤더에 Authorization
필드를 명시하여 요청하는 것을 의미합니다.
브라우저에서 리소스 요청 시 XMLHttpRequest
혹은 Fetch
를 통해 요청할 수 있는데 이 API는 별도의 옵션을 지정하지 않으면 인증 관련 헤더를 요청 헤더에 명시하지 않습니다. CORS는 Access-Control-Allow-Credentials
헤더를 지원하기 때문에 AJAX 요청 API에 credentials
모드로 설정하면 CORS에서도 자격 증명 요청을 수행할 수 있습니다.
- XMLHttpRequest
const xhr = new XMLHttpRequest(); xhr.open('GET', 'http://example.com/', true); xhr.withCredentials = true; xhr.send(null);
- Fetch
만약 동일한 Origin 서버에게만 자격 증명을 보내려면
credentials
속성을 ‘same-origin’으로 명시하면 됩니다.fetch('https://example.com', { credentials: 'include' });
CORS 정책 허용 방법
CORS 정책 위반시 나타나는 오류는 브라우저에서만 발생합니다. 가장 좋은 해결 방법은 리소스를 호출하는 원천 서버에서 헤더 내의Access-Control-Allow-Origin
값을 allow 시키는 것이지만 대다수 외부 API 일 경우, 해당 서버의 권한이 없는 서비스이기 때문에 헤더를 제어하는 것이 불가능합니다.
이러한 경우에는 CORS를 해결하기 위해 접근 가능한 동일 서버(Origin Server)에서 외부 리소스 서버에서 리소스를 호출하고 다시 클라이언트에게 전달하는 방법이 있습니다. 이 역할을 하는 서버를 프록시 서버라고 하며, 서버 간 CORS 정책이 발생하지 않는 특성을 활용하여 쉽게 해결할 수 있습니다.
XSS(Cross-site scripting)
크로스 사이트 스크립팅(XSS)이란 웹 페이지를 방문시 방문한 사용자의 브라우저에서 부정한 HTML 태그나 의도적인 악성 JavaScript 등을 삽입하여 공격하는 행위 입니다. 주로 동적으로 HTML을 생성하는 부분에서 취약성이 발생할 수 있습니다. XSS 공격에 의해 다음과 같은 피해가 발생될 수 있습니다.
- 가짜 입력 form에 의해 사용자의 개인 정보를 탈취
- 악의적인 스크립트로 인해 사용자의 쿠키 값 탈취 또는 의도치 않은 HTTP 요청 발생
- 부적절한 문장 또는 이미지등이 표시
XSS 보안 방법
- 쿠키 값 탈취를 방지하기 위해 쿠키에
HttpOnly
옵션 사용 - 스크립트 관련 문자 이스케이프 처리 (& → &, < → <, > → > 등)
SQL Injection
SQL Injection은 애플리케이션이 운영되기 위해 사용되는 데이터베이스에 부정한 SQL을 입력하는 공격입니다. 허용되지 않은 외부 SQL 입력에 대한 특별한 조치가 없다면 치명적인 영향을 받을 수 있습니다.
- 데이터베이스 내 인가되지 않은 데이터 부정 열람 및 변조
- 인증 회피
- 데이터베이스 서버를 경유한 프로그램 실행
SQL Injection 보안 방법
- 사용자에게 입력 받은 파라미터를 SQL에 전달 할 경우 즉시 조합하지 않고 이스케이프 처리
- 요청 값 유효성 검사
HTTP 아키텍처
URL과 URN
URL은 브라우저가 정보를 찾는데 필요한 리소스의 위치를 가리키는 역할입니다. URL을 사용하여 실제 사용자가 애플리케이션에서 필요한 리소스를 찾고 공유할 수 있습니다. URL은 URI라는 통합 자원 식별자의 한 부분으로써, URI는 URL과 URN으로 구성되어 있습니다
URL 문법
URL은 스킴(HTTP,FTP,SMTP)에 따라서 문법이 달라집니다. 대부분의 스킴은 일반적으로 9개 부분으로 나눠집니다. 스킴은 리소스에 어떻게 접근하는지 알려주는 정보로써 알파벳으로 시작해야하고 URL에서 첫 번째 :
문자로 구분합니다. 스킴은 대소문자 구별을 하지 않습니다.
<스킴>://<사용자이름>:<비밀번호>@<호스트주소>:<포트>/<경로>;<파라미터>?<질의>#<프래그먼트>
컴포넌트 | 설명 | 기본값 |
---|---|---|
스킴 | 리소스를 가져오기 위한 프로토콜 명시 | 없음 |
사용자 이름 | 리소스에 접근 하기 위한 사용자 이름 (몇몇 스킴에서 사용) | anonymous |
비밀번호 | 사용자의 비밀번호 | 이메일 주소 |
호스트 | 리소스를 호스팅하는 서버의 호스트 명 or IP 주소 | 없음 |
포트 | 리소스를 호스팅하는 서버가 열어놓은 비밀번호 HTTP 같은 경우에는 80 |
스킴에 따라 다름 |
경로 | 슬래시(/)로 구분하며, 서버 내 리소스가 어디에 있는지 경로를 가리킴 | 없음 |
파라미터 | 특정 스킴에서 입력 파라미터를 기술하는 용도로 사용 이름/값을 세미콜론으로 구분 |
없음 |
질의 | 애플리케이션에서 파라미터 전달을 위해서 사용(데이터베이스, 게시판, 검색엔진) URL 끝에 ? 문자로 구분 | 없음 |
프래그먼트 | 리소스의 일부분을 가리키는 이름, 클라이언트 영역에서만 사용한다. | 없음 |
URN
urn:oasis:names:specification:docbook:dtd:xml:4.1.2
URL은 리소스를 찾는데 필요한 포트와 서버 별칭을 제공하기 때문에 리소스가 옮겨지면 수동으로 URL도 다시 설정해야한다는 단점이 존재합니다. 이러한 단점을 해결하기 위해 고안한 방식이 URN(Uniform Resource Name)입니다. URL과 달리 리소스 객체가 옮겨지더라도 항상 동일한 리소스를 가리킬 수 있는 이름을 제공합니다.
웹 캐시
웹 캐시는 자주 방문하는 웹 페이지(문서)의 사본을 자동으로 보관하는 HTTP의 기능입니다. 웹 요청이 캐시에 도착했을 때 캐시된 사본이 존재한다면 문서는 원 서버가 아니라 캐시로부터 제공합니다. 캐시를 활용하면 다음과 같은 이점을 얻을 수 있습니다.
- 불필요한 데이터 전송 최소화 및 네트워크 병목 최소화
- 원 서버에 대한 요청 부하 최소화
웹 캐시 동작과정
일반적으로 웹 캐시 처리 절차는 다음과 같습니다.
-
요청
네트워크 커넥션에서 활동을 감지하고 요청오는 데이터를 읽는 단계 입니다. -
파싱
요청 메시지를 여러 부분으로 파싱하여 헤더 부분을 조작하기 쉬운 자료 구조로 담는 단계 입니다. -
검색
캐시는 URL을 통해 해당하는 로컬 사본이 존재하는지 검사합니다. 캐시된 사본은 메모리, 디스크 또는 외부 저장소에 저장될 수 있습니다. 고성능 캐시는 빠른 로드를 위해 적절한 알고리즘을 사용하며, 상황에 따라 원 서버나 부모 프락시에서 가져올 수도 있습니다. -
신선도 검사
HTTP는 캐시가 일정 시간 동안 리소스 사본을 보유할 수 있도록 합니다. 설정된 기간이 초과되면, 신선하지 않은 리소스로 간주하여 서버와 문서를 다시 검사합니다. 검사 후 리소스가 변경되었다면, 캐시는 새로운 사본을 가져와 기존 사본을 덮어쓰고 클라이언트에게 응답합니다.(각 리소스의 유효 기간은
Expires
와Cache-Control
헤더를 통해 설정할 수 있습니다.) -
응답 생성
캐시된 응답을 원 서버에서 온 것처럼 일관성을 지켜야하기 때문에, 캐시된 서버 응답을 토대로 응답 헤더를 생성합니다. -
전송
응답 헤더가 완성되면 클라이언트에게 돌려줍니다. 일반 HTTP 통신과 마찬가지로 캐시는 클라이언트와 커넥션을 유지합니다.
웹 캐시 제어
리소스가 만료되기 전까지 얼마나 오랫동안 캐시를 유지할 것인지 Cache-Control
헤더를 통해 명시할 수 있습니다.
Header Set Cache-Control "max-age=60"
- no-store: 캐시에 리소스를 저장하지 않음
- no-cache: 항상 원 서버에 리소스를 재검사하도록 요청
- must-revalidate: 캐시 만료기간이 지났다면 원 서버어게 리소스를 재검사하도록 요청
- public: 공유 캐시에 저장함
- private: 공유 캐시에 저장하지 않음 (브라우저의 로컬 캐시에만 저장)
- max-age: 캐시의 만료기간 (초)
프록시
프록시란 클라이언트와 서버 사이에서 HTTP 통신을 중개하는 일종의 중개자입니다. 프록시를 사용하는 이유는 주로 보안성, 성능, 비용 절감 등의 이점 때문에 사용합니다. 프록시는 목적에 따라 정방향 프록시와 역방향 프록시로 분류할 수 있습니다.
- 보안성
일부 조직(학교, 정부기관)에서는 부적절한 컨텐츠를 차단하기 위해 제한된 영역의 인터넷에 대해서만 접근 권한을 부여합니다. 프록시를 사용하면 제한된 웹 리소스에 대해 단일 접근 전략을 쉽게 구성할 수 있습니다. - 프록시 캐시
웹 캐싱을 위해 프록시 서버를 활용하면 리소스 사본을 효율적으로 관리할 수 있고, 원 서버에 직접 접근하는 대신 근처의 프록시 캐시 서버로 접근함으로써 요청에 대한 비용을 줄이고 접근 성능을 개선할 수 있습니다.
정방향 프록시(Forward Proxy)
출처: Cloudflare - Forward Proxy Flow
정방향 프록시는 일반적인 프록시 서버를 의미합니다. 이 서버는 인터넷 통신 앞에 위치하여 원본 서버가 특정 리소스 위치에 대한 필터링이나 접근 권한을 설정할 수 있게 해줍니다. 또한 사용자 개별 IP를 우회하여 신원 보안에도 이점을 가질 수 있습니다.
역방향 프록시(Reverse Proxy) 출처: Cloudflare - Reverse Proxy Flow 역방향 프록시는 정방향 프록시와는 반대 개념으로, 원 서버 위치의 앞에 위치한 프록시 구조입니다. 이는 원 서버로 들어오는 트래픽을 분산시키는 효율적인 구조이며, 역방향 프록시도 캐싱을 수행할 수 있어 성능적인 이점도 가질 수 있는 특징이 있습니다.
REST API
REST(Representational State Transfer)의 의미는 네트워크 리소스가 정의되고 처리되는 방식을 일련의 원칙을 기반으로하는 API 작동 방식에 대한 일종의 아키텍처 입니다. REST 아키텍처는 몇 가지 원칙이 존재하는데 이러한 원칙을 따르는 API를 REST API(혹은 RESTful API)라고 지칭합니다.
무상태
클라이언트는 임의의 순서로 리소스를 요청할 수 있으며 모든 요청은 무상태이거나 다른 요청과 분리됩니다. 이전 요청과 관계 없이 서버가 매번 요청을 완전히 이해할 수 있어야 합니다.
계층화 시스템
서버는 요청을 다른 서버로 전달하거나 협력할 수 있습니다. 이러한 특성을 유지하기 위해 보안 및 여러 애플리케이션 및 비즈니스 로직 등을 여러 계층으로 설계할 수 있습니다.
캐싱
REST API를 적용한 웹 서비스는 클라이언트 또는 프록시 서버에 일부 리소스 사본을 저장하는 캐싱을 지원하여 효율적인 응답 시간을 달성할 수 있습니다. 요청 상황에 따라 적절히 캐시 또는 캐시 불가능 상황을 사용하여 제어할 수 있습니다.
균일한 인터페이스
균일한 인터페이스란 서버가 표준 형식으로 리소스(정보)를 전송하는 것을 나타냅니다. 서버 내에 저장되어 있는 실제 리소스의 표현(데이터 형식)과는 다를 수 있습니다. 예를 들어, 데이터는 텍스트로 저장되지만 HTML 문서 형태로 전송할 수 있습니다.
균일한 인터페이스는 아래 4가지 제약 조건을 지키는 것입니다.
- 요청은 리소스를 식별하는 행위입니다. (균일한 리소스를 사용)
- 클라이언트는 원하는 경우 리소스를 수정하거나 삭제하기에 충분한 정보를 확보하기 위해 서버는 리소스에 대한 메타데이터를 전송합니다.
- 클라이언트는 데이터를 추가적으로 처리하는 방법에 대한 정보를 수신할 수 있어야합니다.
- 클라이언트는 하이퍼링크를 통해 더 많은 리소스를 동적으로 검색할 수 있도록 서버에서 하이퍼링크를 전송할 수 있습니다.
REST API의 이점
REST API를 사용하면 클라이언트와 서버가 완전히 분리된 설계를 구축할 수 있습니다. 이는 각 영역이 독립적으로 확장 가능하고 서버 요소들을 다양하게 단순화할 수 있으며, 한 영역의 변경이 다른 영역에 영향을 주지 않습니다. 이를 통해 API 설계에 영향을 받지 않고 다양한 프로그래밍 언어로 서비스를 제공할 수 있으며, 애플리케이션 로직과 관련 없이 데이터베이스 계층도 쉽게 변경할 수 있습니다. (확장성, 유연성, 독립성)