웹 서비스와 보안
HTTP란 무엇인가?
HTTP(HyperText Transfer Protocol)는 웹에서 데이터를 주고받기 위한 애플리케이션 계층 프로토콜입니다.
현재는 거의 모든 웹 애플리케이션의 데이터 교환 표준으로 사용되고 있습니다.
백엔드 개발자는 HTTP를 통해 클라이언트와 서버 간의 통신을 처리하는 API를 만듭니다.
HTTP의 특징
HTTP의 주요 특징을 살펴보면:
-
무상태성(Stateless): HTTP는 상태를 유지하지 않습니다. 각 요청은 이전 요청과 독립적으로 처리됩니다. 이는 서버 확장성에 유리하지만, 사용자 세션 관리에는 추가 메커니즘(쿠키, 세션, 토큰 등)이 필요합니다.
-
비연결성(Connectionless): 기본적으로 요청과 응답 후 연결을 종료합니다. (Keep-Alive 헤더를 통해 연결을 유지하여 여러 요청을 처리할 수 있으나, 본질적으로는 비연결 지향입니다.)
-
클라이언트-서버 구조: 클라이언트가 요청을 보내고 서버가 응답하는 단순한 구조입니다.
-
텍스트 기반 프로토콜: HTTP 메시지는 사람이 읽을 수 있는 텍스트 형식이라 디버깅에 용이합니다.
위 그림은 HTTP 메시지 모습입니다.
HTTP 통신 과정
HTTP 통신 과정은 다음과 같습니다:
-
클라이언트가 TCP 연결 수립: HTTP는 일반적으로 TCP 위에서 동작합니다. 클라이언트는 서버와 TCP 3-way handshake를 통해 연결을 맺습니다. (HTTP/3는 QUIC 프로토콜을 사용하여 UDP 기반으로 동작)
-
HTTP 요청 전송: 클라이언트가 서버에 HTTP 요청을 보냅니다.
-
서버 요청 처리: 서버는 요청을 받아 처리합니다.
-
HTTP 응답 전송: 서버가 클라이언트에게 응답을 보냅니다.
-
연결 종료 또는 유지: HTTP/1.0은 기본적으로 연결을 종료하고, HTTP/1.1 이상은 Keep-Alive가 기본 설정으로 연결을 유지합니다.
위 그림은 TCP 3-way handshake를 사용하여 연결하는 과정입니다.
HTTP 요청 구조
HTTP 요청은 다음과 같은 구조로 이루어져 있습니다:
[요청 라인] - HTTP 메서드, URI, HTTP 버전
[헤더 필드] - 요청에 대한 메타데이터
[공백 라인] - 헤더의 끝을 표시하는 빈 줄
[메시지 본문] - 요청과 함께 전송되는 데이터(선택적)
예시:
GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
Content-Type: application/json
{"name": "John"}
백엔드 개발자는 특히 다음 부분에 주목해야 합니다:
- 메서드(Method): 요청의 의도를 나타냅니다. GET, POST, PUT, DELETE 등이 있습니다.
- 헤더(Headers): 요청에 대한 추가 정보를 제공합니다. 인증 토큰, 컨텐츠 타입, 캐시 설정 등을 포함할 수 있습니다.
- 본문(Body): 주로 POST, PUT과 같은 요청에서 서버로 전송할 데이터를 담습니다.
HTTP 응답 구조
HTTP 응답은 다음과 같은 구조로 이루어져 있습니다:
[상태 라인] - HTTP 버전, 상태 코드, 상태 메시지
[헤더 필드] - 응답에 대한 메타데이터
[공백 라인] - 헤더의 끝을 표시하는 빈 줄
[메시지 본문] - 응답 데이터
예시:
HTTP/1.1 200 OK
Date: Tue, 8 Apr 2025 05:28:43 GMT
Server: Apache/2.4.1
Content-Type: application/json
Content-Length: 23
{"status": "success"}
백엔드 개발자는 특히 다음 부분에 주목해야 합니다:
- 상태 코드(Status Code): 요청 처리 결과를 나타냅니다. 2XX(성공), 3XX(리다이렉션), 4XX(클라이언트 오류), 5XX(서버 오류) 등이 있습니다.
- 헤더(Headers): 응답에 대한 추가 정보를 제공합니다. 컨텐츠 타입, 캐싱 정책, CORS 헤더 등이 포함될 수 있습니다.
- 본문(Body): 서버에서 클라이언트로 전송되는 실제 데이터입니다.
HTTP 헤더
HTTP 헤더는 요청과 응답에 대한 추가 정보를 제공합니다. 백엔드 개발자가 알아야 할 주요 헤더는 다음과 같습니다:
- Content-Type: 요청/응답 본문의 미디어 타입을 지정합니다. 예:
application/json
,text/html
- Authorization: 클라이언트 인증 정보를 전달합니다. 예:
Bearer TOKEN
- Cache-Control: 캐싱 정책을 제어합니다. 예:
no-cache
,max-age=3600
- Accept: 클라이언트가 처리할 수 있는 컨텐츠 타입을 지정합니다.
- Cookie/Set-Cookie: 클라이언트-서버 간 상태 정보를 교환합니다.
- Access-Control-Allow-Origin: CORS(Cross-Origin Resource Sharing) 정책을 설정합니다.
HTTP 버전별 차이점
-
HTTP/1.0: 기본적인 요청-응답 모델을 제공했지만 연결당 하나의 요청-응답 쌍만 처리 가능합니다.
-
HTTP/1.1: Keep-Alive, 파이프라이닝, 청크 전송 인코딩 등을 도입했습니다. 그러나 여전히 Head-of-Line 블로킹 문제가 있습니다.
-
HTTP/2: 바이너리 프로토콜로 전환하여 다중화된 스트림, 서버 푸시, 헤더 압축 등을 지원합니다. 성능이 크게 향상되었지만 여전히 TCP의 Head-of-Line 블로킹 문제가 존재합니다.
-
HTTP/3: QUIC 프로토콜 기반으로 UDP를 사용하여 TCP의 Head-of-Line 블로킹 문제를 해결하고, 연결 설정 지연을 줄였습니다.
HTTP의 한계와 극복 방안
- 무상태성으로 인한 세션 관리 문제 -> 쿠키, 세션, JWT 등을 활용한 상태 관리
- 보안 취약점 -> HTTPS를 통한 암호화 통신
- 단방향 통신 한계 -> WebSocket, Server-Sent Events 등의 실시간 통신 기술
- 성능 이슈 -> HTTP/2, HTTP/3, CDN 활용 등을 통한 성능 최적화
HTTPS란?
HTTPS(Hypertext Transfer Protocol Secure)는 HTTP 프로토콜의 보안 버전으로, 웹 브라우저와 서버 간의 통신을 암호화하여 데이터의 기밀성, 무결성, 인증을 보장합니다. HTTPS는 기존 HTTP 프로토콜에 SSL/TLS(Secure Sockets Layer/Transport Layer Security) 프로토콜을 결합한 형태입니다.
HTTPS는 기본적으로 HTTP와 동일한 요청/응답 메커니즘을 사용하지만, 모든 데이터가 암호화된 상태로 전송됩니다. 표준 HTTP는 포트 80을 사용하는 반면, HTTPS는 기본적으로 포트 443을 사용합니다.
HTTPS가 해결하는 HTTP의 보안 문제
기존 HTTP 프로토콜은 다음과 같은 보안 취약점을 가지고 있습니다.
- 도청(Eavesdropping): HTTP 통신은 평문(plaintext)으로 이루어지기 때문에, 네트워크 상에서 패킷을 가로채면 내용을 확인할 수 있습니다.
- 변조(Tampering): 중간자(Man-in-the-Middle) 공격을 통해 전송 중인 데이터를 수정할 수 있습니다.
- 위장(Spoofing): 공격자가 합법적인 웹사이트인 것처럼 위장하여 사용자의 민감한 정보를 탈취할 수 있습니다.
HTTPS는 이런 문제들을 암호화와 인증을 통해 해결합니다.
SSL/TLS
SSL과 TLS의 관계
- SSL(Secure Sockets Layer): Netscape에서 개발한 최초의 보안 프로토콜로, 버전은 1.0, 2.0, 3.0까지 있습니다.
- TLS(Transport Layer Security): SSL의 후속 버전으로, IETF에 의해 표준화되었습니다. TLS 1.0은 SSL 3.0의 업그레이드 버전입니다.
현재는 SSL 3.0까지 모두 보안 취약점이 발견되어 사용이 권장되지 않으며, TLS가 표준으로 사용됩니다. 하지만 관행적으로 아직도 “SSL 인증서” 같은 용어가 사용됩니다.
최신 TLS 버전
- TLS 1.0, 1.1: 더 이상 안전하지 않아 사용이 권장되지 않음
- TLS 1.2: 널리 사용되는 안전한 버전
- TLS 1.3(2018년 출시): 가장 최신 버전으로, 핸드셰이크 과정을 간소화하고 보안을 강화함
SSL/TLS가 제공하는 보안 기능
- 기밀성(Confidentiality): 대칭 키 암호화를 사용하여 데이터를 암호화함으로써 제3자가 통신 내용을 읽을 수 없게 합니다.
- 무결성(Integrity): MAC(Message Authentication Code)을 사용하여 전송 중 데이터 변조를 방지합니다.
- 인증(Authentication): 인증서와 공개 키 기반구조(PKI)를 사용하여 서버(경우에 따라 클라이언트)의 신원을 확인합니다.
SSL/TLS 암호화 과정
비대칭 키 암호화와 대칭 키 암호화의 결합
SSL/TLS는 두 가지 암호화 방식을 함께 사용합니다:
- 비대칭 키 암호화(Asymmetric Encryption): 공개 키와 개인 키 쌍을 사용하며, 핸드셰이크 과정에서 대칭 키를 안전하게 교환하는 데 사용됩니다. 계산 비용이 높지만 보안성이 뛰어납니다.
- 대칭 키 암호화(Symmetric Encryption): 동일한 키로 암호화와 복호화를 수행하며, 실제 데이터 전송 시 사용됩니다. 계산 비용이 낮고 속도가 빠릅니다.
이 두 방식을 결합함으로써, SSL/TLS는 보안성과 성능 간의 균형을 유지합니다.
TLS 핸드셰이크 과정
TLS 핸드셰이크는 보안 연결을 설정하기 위한 단계로, 다음과 같이 진행됩니다.
TLS 1.2 핸드셰이크
- ClientHello:
- 클라이언트가 서버에 연결을 시도하며 지원하는 암호화 알고리즘(cipher suite) 목록, 랜덤 데이터(client random), 세션 ID 등을 전송합니다.
- ServerHello:
- 서버가 클라이언트의 제안 중에서 가장 강력한 암호화 알고리즘을 선택하고, 자신의 랜덤 데이터(server random)를 전송합니다.
- Certificate:
- 서버가 자신의 SSL/TLS 인증서를 클라이언트에게 전송합니다.
- 이 인증서에는 서버의 공개 키와 CA(Certificate Authority)의 디지털 서명이 포함되어 있습니다.
- ServerKeyExchange (필요한 경우):
- 일부 암호화 알고리즘에서 서버가 추가적인 키 교환 정보를 전송합니다.
- CertificateRequest (선택적):
- 서버가 클라이언트 인증서를 요청할 수 있습니다(상호 인증).
- ServerHelloDone:
- 서버가 자신의 초기 메시지 전송을 완료했음을 알립니다.
- ClientKeyExchange:
- 클라이언트가 pre-master secret을 생성하고, 서버의 공개 키로 암호화하여 전송합니다.
- 클라이언트와 서버는 client random, server random, pre-master secret을 사용하여 각각 동일한 master secret을 생성합니다.
- 이 master secret에서 세션 키(대칭 키)가 파생됩니다.
- ChangeCipherSpec:
- 클라이언트가 이제부터 합의된 대칭 키로 암호화된 메시지를 보낼 것임을 알립니다.
- Finished:
- 클라이언트가 지금까지의 핸드셰이크 메시지의 해시를 암호화하여 전송합니다.
- ChangeCipherSpec:
- 서버도 대칭 키 암호화로 전환함을 알립니다.
- Finished:
- 서버가 핸드셰이크 메시지의 해시를 암호화하여 전송합니다.
- 클라이언트와 서버가 이 해시를 확인하여 핸드셰이크의 무결성을 검증합니다.
이제 통신 채널이 수립되어, 애플리케이션 데이터를 암호화하여 주고받을 수 있습니다.
TLS 1.3 핸드셰이크 (간소화된 최신 버전)
TLS 1.3에서는 핸드셰이크 과정이 1-RTT(Round Trip Time)로 단축되었습니다:
- ClientHello:
- 클라이언트가 자신의 암호화 알고리즘 목록뿐만 아니라, 가능한 키 교환 매개변수를 포함한 “키 공유(key share)” 정보도 함께 전송합니다.
- ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished:
- 서버가 한 번에 모든 응답을 보냅니다.
- 이미 클라이언트의 키 공유 정보를 받았기 때문에, 바로 키를 계산하고 통신을 시작할 수 있습니다.
- Finished:
- 클라이언트가 확인 메시지를 보내고 애플리케이션 데이터 교환을 시작합니다.
TLS 1.3은 보안이 낮은 알고리즘을 제거하고, 핸드셰이크 과정을 간소화하여 더 빠르고 안전한 연결을 제공합니다.
SSL/TLS 인증서의 구조
SSL/TLS 인증서는 아래와 같은 정보를 포함합니다.
- 웹사이트 도메인 이름(Common Name)
- 인증서 소유자 정보
- 발급 기관(CA) 정보
- 발급 일자와 만료 일자
- 공개 키
- 디지털 서명
- 확장 필드(SAN, 키 사용 등)
인증서 검증 과정
브라우저가 서버의 인증서를 검증하는 과정은 다음과 같습니다.
- 인증서의 디지털 서명을 발급 CA의 공개 키로 검증
- 인증서의 만료 여부 확인
- 인증서가 해지되었는지 확인
- 도메인 이름과 인증서의 “웹사이트 도메인 이름(Common Name)” 또는 “SAN(Subject Alternative Name)” 일치 여부 확인
- 인증서 체인 검증
서버 개발시 HTTPS 적용하기
웹 서버(Nginx, Apache 등)에서 HTTPS를 적용하려면 다음이 필요합니다.
- 인증서 취득:
- Let’s Encrypt와 같은 무료 CA 또는 상용 CA에서 인증서 발급
- 자체 서명된 인증서(개발 환경용)
- 서버 구성:
# Nginx 예시 server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/certificate.crt; ssl_certificate_key /path/to/private.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # ... 나머지 구성 }
- HTTP에서 HTTPS로 리다이렉션:
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }
일반적인 HTTPS 문제 해결법
- 인증서와 도메인 불일치:
- 인증서의 CN/SAN이 실제 접속 도메인과 일치하지 않는 경우
- 해결: 올바른 도메인에 대한 인증서 발급 또는 SAN에 필요한 도메인 추가
- 인증서 만료:
- 해결: 자동 갱신 설정
- 혼합 콘텐츠(Mixed Content) 경고:
- HTTPS 페이지 내에 HTTP 리소스 포함 시 발생
- 해결: 모든 리소스를 HTTPS로 제공
DNS(Domain Name System)란
DNS는 인터넷의 전화번호부와 같은 역할을 합니다. 사람이 기억하기 쉬운 도메인 이름(예: www.example.com)을 컴퓨터가 인식할 수 있는 IP 주소(예: 192.0.2.1)로 변환해주는 시스템입니다.
DNS 서버의 종류
- 루트 DNS 서버 (Root DNS Server): 최상위 DNS 서버로 TLD DNS 서버 정보 제공 (ICANN과 여러 협력 기관들이 관리)
- TLD DNS 서버: .com, .net, .org 등의 URL의 마지막 점 뒤에 오는 최상위 도메인(Top-Level Domain) 관리. (ICANN과 여러 협력 기관들이 관리)
- 권한 있는 DNS 서버 (Authoritative DNS Server): 실제 도메인의 DNS 정보를 직접 관리. 최종 IP 주소를 반환한다. (도메인 등록 업체(AWS Route 53, Google DNS) 또는 호스팅 업체가 관리)
- 리커시브 DNS 서버 (Recursive Resolver): 클라이언트 대신 DNS 질의를 수행하는 서버. (ISP 또는 독립 DNS 서비스 제공업체(AWS Route 53, Google DNS)가 관리)
DNS 질의 과정
재귀적 질의(Recursive Query)
재귀적 질의는 클라이언트가 DNS 리졸버에게 요청하면, 리졸버가 답을 찾을 때까지 다른 DNS 서버들을 대신 질의하여 최종 결과를 클라이언트에게 반환하는 방식입니다.
과정:
- 클라이언트가 DNS 리졸버(리커시브 DNS 서버)에 도메인 이름에 대한 IP 주소를 요청합니다.
- DNS 리졸버는 우선 자체 캐시를 확인합니다.
- 캐시에 없다면, 루트 DNS 서버에 질의합니다.
- 루트 DNS 서버는 해당 TLD(.com, .org 등)를 담당하는 DNS 서버 정보를 제공합니다.
- DNS 리졸버는 TLD DNS 서버에 질의합니다.
- TLD DNS 서버는 도메인에 대한 권한 있는 네임서버 정보를 제공합니다.
- DNS 리졸버는 권한 있는 네임서버에 질의합니다.
- 권한 있는 네임서버가 최종 IP 주소를 DNS 리졸버에 반환합니다.
- DNS 리졸버는 이 정보를 캐싱하고 클라이언트에게 전달합니다.
반복적 질의(Iterative Query)
반복적 질의는 DNS 리졸버가 답을 모르는 경우 “나는 모르지만, 여기에 물어보세요”라고 다음 질의할 서버 정보만 알려주는 방식입니다.
과정:
- 클라이언트가 DNS 서버에 질의합니다.
- DNS 서버는 자체 캐시나 영역에 정보가 없으면, 다음에 질의할 DNS 서버 정보만 반환합니다.
- 클라이언트는 반환된 DNS 서버에 다시 질의합니다.
- 이 과정을 답을 얻을 때까지 반복합니다.
비교 요약:
Iterative Query: 클라이언트가 여러 번 서버에 요청을 보내는 방식 (서버는 중간 결과를 제공하고, 클라이언트는 최종 결과를 찾기 위해 다시 요청을 보냄).
Recursive Query: 서버가 요청을 처리하고 최종 결과를 클라이언트에게 직접 제공하는 방식 (서버가 모든 중간 단계 처리를 대신 함).
현실에서는 위 그림처럼 클라이언트가 리커시브 DNS 서버에 재귀적 질의를 요청하고, 리커시브 DNS 서버는 다른 DNS 서버들에게 반복적 질의를 수행하는 하이브리드 방식이 일반적입니다.
DNS 캐싱
DNS 캐싱은 DNS 질의 결과를 일정 시간 동안 저장하여 반복적인 요청 시 빠르게 응답하는 메커니즘입니다.
- TTL(Time To Live): DNS 레코드가 캐시에 얼마나 오래 유지될지 정의하는 값(초 단위)
- 네가티브 캐싱: 존재하지 않는 도메인 질의 결과도 캐싱하여 불필요한 반복 질의 방지
캐싱 계층
- 브라우저 캐시: 웹 브라우저 자체의 DNS 캐시
- OS 캐시: 운영체제 수준의 DNS 캐시
- 리커시브 DNS 서버 캐시: ISP나 공용 DNS 서버의 캐시
- 권한 있는 서버 캐시: 최종 도메인 정보를 가진 서버의 캐시
개발 시 참고 할 DNS 관련 개념
DNS 전파 시간(DNS Propagation)
DNS 레코드 변경 시 인터넷 전체에 변경사항이 적용되는 데 걸리는 시간입니다. TTL 설정에 따라 다르지만, 일반적으로 몇 시간에서 48시간까지 소요될 수 있습니다. 배포 계획 시 이 시간을 고려해야 합니다.
DNS와 로드밸런싱
- 라운드 로빈 DNS: 하나의 도메인에 여러 IP 주소를 연결하여 부하 분산
- 지역 기반 DNS: 사용자의 지리적 위치에 따라 가장 가까운 서버로 연결
DNS 보안
- DNSSEC(DNS Security Extensions): DNS 데이터의 무결성과 출처 인증을 보장하는 보안 확장 기능
- DNS over HTTPS(DoH): HTTPS를 통해 DNS 쿼리를 암호화하여 중간자 공격 방지
- DNS over TLS(DoT): TLS를 통한 DNS 쿼리 암호화
도메인 이름 설계
- 서브도메인 활용: api.example.com, admin.example.com 등으로 서비스 분리
- 다중 도메인 관리: 국가별, 서비스별 도메인 전략
- 개발/스테이징 환경 분리: dev.example.com, staging.example.com
HTTP의 무상태성(Stateless)
HTTP는 기본적으로 무상태(Stateless) 프로토콜입니다. 각 요청은 독립적으로 처리되며, 서버는 이전 요청에 대한 정보를 기억하지 않습니다. 이런 특성은 서버 확장성에 유리하지만, 사용자 인증이나 장바구니 기능과 같이 상태 유지가 필요한 서비스에서는 제약이 됩니다. 즉 HTTP 프로토콜은 “stateless”한 특성을 가지기 때문에 서버는 클라이언트가 누구인지 매번 확인해야 합니다. 이를 해결하기 위해 쿠키, 세션, JWT와 같은 상태 유지 기술이 사용됩니다.
쿠키(Cookie)
쿠키는 서버가 클라이언트의 웹 브라우저에 저장하는 작은 데이터 조각입니다. 클라이언트는 동일한 서버에 요청을 보낼 때마다 저장된 쿠키를 함께 전송합니다.
동작 원리
서버는 HTTP 응답 헤더의 Set-Cookie
를 통해 쿠키 생성을 지시합니다. 브라우저는 쿠키를 저장하고 이후 요청에서 브라우저는 Cookie
헤더에 쿠키를 포함시켜 서버로 전송합니다.
쿠키 속성
Domain
: 쿠키가 전송될 도메인 지정Path
: 쿠키가 유효한 경로 지정Expires
/Max-Age
: 쿠키 만료 시간 설정Secure
: HTTPS 연결에서만 쿠키 전송HttpOnly
: JavaScript를 통한 쿠키 접근 방지 (XSS 공격 방어)SameSite
: CSRF 공격 방어를 위한 설정 (None, Lax, Strict)
참고
- 쿠키는 클라이언트 측에 저장되어 변조될 수 있으므로, 민감한 정보는 저장하지 않아야 함
HttpOnly
와Secure
속성을 적절히 사용하여 보안 강화- 쿠키 크기는 일반적으로 4KB로 제한됨
- 사이트당 쿠키 개수도 브라우저마다 제한이 있음 (약 50-75개)
// Spring Boot 쿠키 설정 간단 예시
@RestController
public class CookieController {
@GetMapping("/setCookie")
public ResponseEntity<String> setCookie() {
// 쿠키 생성
ResponseCookie cookie = ResponseCookie.from("sessionId", "abc123")
.httpOnly(true) // 클라이언트에서 JavaScript로 접근할 수 없게 설정 (보안 목적)
.secure(true) // HTTPS 연결에서만 쿠키를 전송하도록 설정
.maxAge(Duration.ofHours(1)) // 쿠키의 만료시간을 1시간으로 설정
.sameSite("Strict") // 동일 사이트에서만 쿠키를 전송하도록 설정
.path("/") // 쿠키가 유효한 경로 설정 (전체 경로에 대해 유효)
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString()) // 응답 헤더에 쿠키를 설정
.body("쿠키가 설정되었습니다"); // 응답 본문에 메시지를 담음
}
@GetMapping("/getCookie")
public String getCookie(@CookieValue(name = "sessionId", required = false) String sessionId) {
if (sessionId != null) {
return "쿠키 값: " + sessionId; // 쿠키가 있으면 쿠키 값을 반환
} else {
return "쿠키가 없습니다"; // 쿠키가 없으면 없는 경우 처리
}
}
}
쿠키 전송 예입니다.
이 헤더는 클라이언트에게 쿠키를 저장하라고 알려줍니다.
이제 서버에 대한 각각의 새로운 요청마다 브라우저는 Cookie 헤더를 사용하여 이전에 저장된 모든 쿠키를 서버로 다시 전송합니다.
세션(Session)
세션은 서버 측에서 사용자 상태를 유지하는 방법으로, 세션 식별자만 클라이언트에 저장하고 실제 데이터는 서버에 보관합니다.
동작 원리
사용자가 처음 접속하면 서버는 유니크한 세션 ID 생성합니다. 이후 세션 ID를 쿠키에 저장하여 클라이언트에 전송하며 세션 ID에 해당하는 사용자 데이터를 메모리, 데이터베이스 등에 저장합니다. 클라이언트가 재접속하면 쿠키의 세션 ID를 통해 서버에서 해당 사용자 데이터 조회합니다.
세션 저장소
- 메모리: 빠르지만 서버 재시작 시 모든 세션 손실, 확장성 제약
- 데이터베이스: 영구적이지만 속도 저하
- Redis와 같은 인메모리 데이터 스토어: 빠른 접근과 영속성 사이의 균형
참고
- 세션은 서버 자원을 사용하므로 많은 동시 사용자가 있을 때 메모리 부하 발생
- 세션 만료 시간 설정으로 리소스 관리 필요
- 수평적 확장(서버 여러 대) 시 세션 공유 전략 필요 (Sticky Session, 중앙화된 세션 저장소)
- 세션 하이재킹 공격 방지를 위한 대책 필요
// Spring Boot 세션 사용 예시
/* application.properties 또는 application.yml 설정 */
// 메모리 세션 저장소 설정
spring.session.store-type=none
// 또는 Redis를 세션 저장소로 설정
spring.session.store-type=redis
spring.redis.host=localhost // Redis 서버 호스트 설정
spring.redis.port=6379 // Redis 서버 포트 설정
// 세션 타임아웃 설정 (초 단위, 30분)
spring.session.timeout=1800
/* SessionConfig.java */
@Configuration
@EnableRedisHttpSession // Redis 세션 저장소 사용을 활성화
public class SessionConfig {
// Redis 세션 저장소 연결을 위한 빈을 설정
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(); // Lettuce는 Redis 클라이언트 중 하나
}
}
/* SessionController.java */
@RestController // 이 클래스는 REST API를 처리하는 컨트롤러임을 나타냄
public class SessionController {
// 로그인 처리 메서드
@PostMapping("/login")
public String login(HttpSession session, @RequestBody LoginDto loginDto) {
// 사용자 인증 로직 (생략)
User user = userService.authenticate(loginDto); // 사용자 인증 수행
if (user != null) {
// 인증 성공 시 세션에 사용자 정보 저장
session.setAttribute("userId", user.getId()); // 세션에 사용자 ID 저장
session.setAttribute("isAuthenticated", true); // 세션에 인증 상태 저장
// 세션 타임아웃 설정 (30분)
session.setMaxInactiveInterval(1800); // 세션의 비활성화 시간 (30분)
return "로그인 성공";
}
return "로그인 실패";
}
// 프로필 조회 메서드
@GetMapping("/profile")
public String profile(HttpSession session) {
Long userId = (Long) session.getAttribute("userId"); // 세션에서 사용자 ID 가져오기
Boolean isAuthenticated = (Boolean) session.getAttribute("isAuthenticated"); // 세션에서 인증 상태 가져오기
if (userId != null && isAuthenticated != null && isAuthenticated) {
// 인증된 사용자일 경우
return "사용자 프로필: " + userId;
}
return "인증이 필요합니다";
}
// 로그아웃 처리 메서드
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 세션을 무효화하여 로그아웃 처리
return "로그아웃 성공";
}
}
JWT (JSON Web Token)
JWT는 당사자 간에 정보를 안전하게 JSON 객체로 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준(RFC 7519)입니다. 이 정보는 디지털 서명되어 있어 신뢰할 수 있습니다.
JWT 구조
- 헤더(Header): 토큰 유형과 사용된 서명 알고리즘 지정
{ "alg": "HS246", "typ": "JWT" }
- 페이로드(Payload): 클레임(claim)이라 불리는 엔티티와 추가 데이터 포함
{ "sub": "1234567890", // subject "name": "Bob", // custom claim "iat": 1516239022, // issued at "exp": 1516242622 // expiration time }
- 서명(Signature): 헤더와 페이로드를 Base64Url로 인코딩한 후 비밀키로 서명
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
동작 원리
사용자 인증 후 서버가 JWT 생성해 클라이언트에 반환합니다. 클라이언트는 모든 요청 시 Authorization 헤더에 JWT 포함하며 서버는 JWT 검증으로 요청의 인증/권한 확인합니다. (토큰 만료 시 재발급 필요)
토큰 저장 위치
- 일반적으로 localStorage나 sessionStorage에 저장
- XSS 공격에 취약할 수 있어 httpOnly 쿠키 사용도 고려
참고
- JWT는 서명되어 있지만 암호화되지 않았으므로 민감 정보 포함 금지
- 서버 측에서 토큰 무효화가 어려움 (토큰 블랙리스트 구현 필요)
- 토큰 크기가 쿠키보다 클 수 있어 요청 크기 증가
- Refresh Token과 Access Token 패턴
- Access Token: 짧은 유효기간, 리소스 접근 용도
- Refresh Token: 긴 유효기간, 새 Access Token 발급 용도
(jwt 구현할 때 개발자 유미👈님께서 잘 정리해주신게 있으니 참고)
기술 비교 및 선택 기준
쿠키 vs 세션
- 보안: 세션이 쿠키보다 안전 (데이터가 서버에 저장)
- 성능: 쿠키가 서버 부하 적음, 세션은 서버 리소스 사용
- 확장성: 쿠키가 확장성에 유리 (서버 상태 저장 안 함)
- 데이터 크기: 쿠키는 4KB 제한, 세션은 서버 한도에 따라 다름
세션 vs JWT
- 상태: 세션은 서버에 상태 저장(stateful), JWT는 무상태(stateless)
- 확장성: JWT가 수평 확장에 유리
- 오버헤드: JWT는 매 요청마다 암호화 검증 필요
- 무효화: 세션은 즉시 무효화 가능, JWT는 어려움
선택 기준
- 단순한 사용자 추적: 쿠키
- 보안이 중요한 사용자 인증: 세션
- 마이크로서비스/분산 시스템: JWT
- 모바일 API: JWT
보안 고려사항
CSRF(Cross-Site Request Forgery) 방어
- 쿠키: SameSite 속성, CSRF 토큰
- JWT: Authorization 헤더 사용 (쿠키에 저장 시 추가 대책 필요)
XSS(Cross-Site Scripting) 방어
- 쿠키: HttpOnly 속성
- JWT: localStorage 사용 시 취약, HttpOnly 쿠키에 저장 고려
XSS (Cross-Site Scripting)(사이트 간 스크립팅)
개념
XSS는 공격자가 웹 사이트에 악성 클라이언트 측 스크립트를 삽입하여 다른 사용자의 브라우저에서 실행되게 하는 공격 기법입니다. 이 공격이 성공하면 공격자는 사용자의 쿠키나 세션 토큰을 탈취하거나, 웹 페이지를 변조하거나, 악성 코드를 실행할 수 있습니다.
XSS 유형
- 저장형(Stored) XSS: 악성 스크립트가 서버에 영구적으로 저장되어 해당 페이지를 방문하는 모든 사용자에게 영향
- 반사형(Reflected) XSS: URL 파라미터 등을 통해 전달된 악성 스크립트가 즉시 페이지에 반영
- DOM 기반 XSS: 클라이언트 측 JavaScript가 DOM을 안전하지 않게 조작할 때 발생
방어 기법
- 입력 검증과 출력 인코딩
// 잘못된 방식 String userInput = request.getParameter("comment"); response.getWriter().println("<div>" + userInput + "</div>"); // 올바른 방식 String userInput = request.getParameter("comment"); String safeInput = StringEscapeUtils.escapeHtml4(userInput); response.getWriter().println("<div>" + safeInput + "</div>");
- 콘텐츠 보안 정책(CSP) 설정
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
- HttpOnly 쿠키 사용: 클라이언트 측 스크립트가 쿠키에 접근할 수 없게 함
Cookie cookie = new Cookie("sessionId", "abc123"); cookie.setHttpOnly(true); response.addCookie(cookie);
- 프레임워크의 보안 기능 활용
- Spring MVC의
th:text
나 React의 ``와 같은 자동 이스케이프 기능 사용
- Spring MVC의
CSRF (Cross-Site Request Forgery)(사이트 간 요청 위조)
개념
CSRF는 인증된 사용자가 의도하지 않은 요청을 서버에 보내도록 속이는 공격입니다. 사용자가 로그인한 상태에서 악성 웹사이트를 방문했을 때, 브라우저에 저장된 쿠키를 이용해 사용자 모르게 서버에 요청을 보내게 됩니다.
방어 기법
- CSRF 토큰 사용
// 서버 측 토큰 생성 String csrfToken = UUID.randomUUID().toString(); session.setAttribute("csrfToken", csrfToken); // 뷰에 토큰 포함 model.addAttribute("csrfToken", csrfToken); // 요청 시 토큰 검증 String receivedToken = request.getParameter("csrfToken"); String expectedToken = (String) session.getAttribute("csrfToken"); if (receivedToken == null || !receivedToken.equals(expectedToken)) { throw new ForbiddenException("CSRF 토큰이 유효하지 않습니다."); }
- SameSite 쿠키 속성 사용
Cookie cookie = new Cookie("sessionId", "abc123"); cookie.setAttribute("SameSite", "Strict"); // 또는 "Lax" response.addCookie(cookie);
- Referer 검증
String referer = request.getHeader("Referer"); if (referer == null || !referer.startsWith("https://mywebsite.com")) { throw new ForbiddenException("올바르지 않은 출처입니다."); }
- Double Submit Cookie: 쿠키와 요청 파라미터로 동일한 값을 전송하고 서버에서 일치 여부 확인
SQL Injection (SQL 삽입)
개념
SQL Injection은 악의적인 SQL 쿼리를 입력하여 데이터베이스를 조작하는 공격 기법입니다. 이 공격으로 데이터 유출, 수정, 삭제는 물론 서버 명령어 실행까지 가능할 수 있습니다.
위 그림에서 보면 쿼리가 데이터베이스 테이블에서 일치하는 ID를 검색했다면, 이제 쿼리는 ID를 찾거나 1이 1과 같은지 테스트합니다. 예상할 수 있듯이 이 문은 열에 있는 모든 선생님에 대해 항상 참이므로, 그 결과 데이터베이스는 선생님 테이블의 모든 데이터를 쿼리를 수행한 공격자에게 반환합니다.
-- 또다른 예시
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
-- 공격자 입력: username = "admin' --"
-- 결과 쿼리: SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- '--' 이후는 주석 처리되어 비밀번호 확인 없이 로그인 가능
방어 기법
- 파라미터화된 쿼리(Prepared Statements) 사용
String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement statement = connection.prepareStatement(sql); statement.setString(1, username); statement.setString(2, password); ResultSet resultSet = statement.executeQuery();
- ORM 프레임워크 활용
// JPA/Hibernate 예시 User user = entityManager .createQuery("from User where username = :username and password = :password", User.class) .setParameter("username", username) .setParameter("password", password) .getSingleResult();
- 입력 값 검증 및 이스케이프
// 특수 문자 이스케이프 String safeInput = input.replaceAll("'", "''");
- 최소 권한 원칙 적용: 데이터베이스 사용자에게 필요한 최소한의 권한만 부여
통합 방어 전략
- 입력 데이터 검증: 모든 사용자 입력은 서버와 클라이언트 양쪽에서 검증
if (!Pattern.matches("[a-zA-Z0-9]{1,20}", username)) { throw new ValidationException("유효하지 않은 사용자명"); }
- 보안 헤더 설정
response.setHeader("X-XSS-Protection", "1; mode=block"); response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("X-Frame-Options", "DENY");
-
웹 애플리케이션 방화벽(WAF) 사용: 일반적인 공격 패턴 차단
- 에러 처리: 상세한 오류 정보를 외부에 노출하지 않음
try { // 데이터베이스 작업 } catch (SQLException e) { logger.error("데이터베이스 오류: ", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."); }
Leave a comment