[실습] 애플리케이션 빌드/실행/배포 실습
Spring Boot 애플리케이션 Build 및 실행 실습 개요
이번 실습은 여러분들이 만든 샘플 애플리케이션을 빌드 후, 실행해 보는 실습입니다
- 빌드 및 애플리케이션 실행 파일을 실행해 보는 실습이므로 별도의 소스 코드 작성은 없습니다.
- 실습용 프로젝트 패키지는 ‘com.springboot’ 패키지 하위에 아래와 같이 구성되어 있습니다.
- advice
- audit
- coffee
- config
- dto
- exception
- member
- order
- response
- stamp
- validator
- 현재의 소스 코드는 API 문서화까지의 작업이 모두 반영되어 있으며, 모든 테스트 케이스 역시 “passed” 되도록 작성되어 있습니다.
실습 사전 준비
- 실습용 샘플 프로젝트 복제
- ⭐ 애플리케이션 빌드 실습 과제는 별도의 제출이 필요 없으므로 단순히 Github Repository를 clone 받아서 실습을 진행하면 됩니다.
- 아래 github 링크에서 실습용 repository를 clone합니다.
- https://github.com/Lucky-kor/be-homework-build
- IntelliJ IDE로 clone 받은 local repository 디렉토리의 프로젝트를 Open합니다.
- 실습 과제 요구 사항에 맞게 실습을 진행합니다.
실습 과제 내용
실습 1: 빌드된 Executable Jar 파일에 프로파일 적용/애플리케이션 실행 실습
- 실습을 위한 설명
- 현재 src/main/resources 경로에는 세 개의 application-xxxx.yml 파일이 존재합니다(application.yml 포함).
- application.yml 파일은 비어있습니다.
- application-local.yml 파일에는 IntelliJ IDE 같은 로컬 환경에서 실행할 설정 정보들이 모두 작성되어 있습니다.
- application-server.yml 파일은 JPA 관련 설정 정보만 작성되어 있습니다.
- 실습 요구 사항
- ⭐ application-server.yml 파일에 여러분들의 PC에 설치된 MySQL 접속 정보를 설정합니다.
- 만약 MySQL이 설치되어 있지 않거나 삭제했다면 다시 설치한 후, 실습을 진행하세요.
- 구글링 등을 통해 Spring Boot 애플리케이션에서 MySQL DB 연동 방법 등을 여러분 스스로 찾아보아야 합니다.
- MySQL 연동 방법을 학습하지는 않았지만 여러분이 이 정도는 스스로 검색해서 연동할 수 있어야 합니다.
- 여러분들이 잘할 거라고 믿습니다.
- 정 힘들다면 [분산 트랜잭션 레퍼런스 코드]에 설정된 MySQL 정보를 일정 부분 활용할 수 있습니다.
- 의존 라이브러리가 필요하다면 의존 라이브러리를 추가해야 할 수도 있습니다.
- ⭐ 애플리케이션 빌드 후, Executable Jar 파일에 프로파일을 적용해서 애플리케이션을 실행한 뒤 애플리케이션에 MySQL이 정상적으로 연동되는지 확인하세요.
- 즉, H2 인메모리 DB를 사용하는 것이 아니라 MySQL이 정상적으로 연동되어 우리가 이제껏 만들어 온 커피 주문 애플리케이션의 회원 정보 저장, 커피 정보 저장, 주문 정보 저장 등의 기능이 정상적으로 동작해야 합니다.
- 여러분이 Jar 파일 실행 시, 프로파일을 적용한 후에 직접 Postman으로 애플리케이션의 API를 호출해서 확인하면 됩니다.
- ⭐ application-server.yml 파일에 여러분들의 PC에 설치된 MySQL 접속 정보를 설정합니다.
- 제한 사항
- 테스트 케이스 작성에 대한 별도의 제한 사항은 없습니다.
application-server.yml
# 서버 환경에서 사용하는 정보들은 application-server.yml 파일에 설정합니다.
# TODO MySQL DB 접속 정보를 아래에 설정하세요
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/example?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: xxxx
jpa:
hibernate:
ddl-auto: create # (1) 스키마 자동 생성
show-sql: true # (2) SQL 쿼리 출력
properties:
hibernate:
format_sql: true # (3) SQL pretty print
build.gradle
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2"
id 'java'
}
group = 'com.springboot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
configurations {
asciidoctorExtensions
}
dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
implementation 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
implementation 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.mapstruct:mapstruct:1.5.1.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.google.code.gson:gson'
}
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
println "asciidoctor output: ${asciidoctor.outputDir}"
from file("${asciidoctor.outputDir}")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn copyDocument
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
Termian에서 ./gradlew clean build를 입력합니다.
build/libs로 들어가서 아래와 같이 명령어를 실행합니다.
Spring이 실행된 것을 확인할 수 있습니다.
POSTMAN에서 데이터를 보냅니다.
데이터가 성공적으로 들어온 것을 확인할 수 있습니다.
[인증/보안] 기초
웹 사이트에 보안은 굉장히 중요한 요소입니다. 보안이 제대로 되어 있지 않다면 여러분들의 개인정보가 악의적인 곳에 유출될 가능성이 있습니다. Spring Framework에서는 Spring Security를 통해 보안 관련 기능을 편리하게 지원하고 있습니다. 이번 유닛에서는 Spring Security를 이용해 인증과 보안을 구현하기에 앞서, 인증과 보안의 기초가 되는 개념들의 Flow를 중심으로 학습합니다.
Before You Learn
- 앞에서 학습한 네트워크 및 HTTP와 관련한 지식이 있어야 합니다.
- 요청과 응답의 형태로 이루어지는 HTTP의 클라이언트 - 서버 모델을 이해하고 있어야 합니다.
- HTTP의 특징인 무상태성에 대해 이해하고 있어야 합니다.
학습 목표
- 암호화와 hashing, salting 등의 개념을 이해할 수 있다.
- HTTP와 HTTPS의 차이점을 이해할 수 있다.
- 권한 부여(Authorization)와 인증(Authentication)에 대해 이해할 수 있다.
- 쿠키의 작동 원리를 이해할 수 있다.
- 클라이언트, 서버, 데이터베이스의 전체 동작을 이해할 수 있다.
- 서비스의 보안과 관련된 방법을 알아보고 원리 및 장점 및 단점을 이해할 수 있다.
HTTPS 개념학습
지금 내가 접속한 사이트가 보안이 된 웹 사이트인지 가장 쉽게 확인하는 방법은 무엇일까요? 여러분이 보고 있는 화면의 주소 바 옆에 있는 자물쇠 아이콘을 눌러보면 됩니다. 해당 아이콘을 누르면 아래와 같이 “이 사이트는 HTTPS를 통해 보호되고 있다”라는 메시지를 볼 수 있습니다.
이렇게 현재 대부분의 웹사이트의 경우 HTTPS를 사용하고 있으며, 만약 HTTP를 사용하는 웹사이트에 접속했다면 ⚠️Not Secure라는 메시지를 표시해 사용자가 해당 웹사이트와의 연결하는 것을 주저하게 만들기도 합니다. 단지 뒤에 S가 붙었을 뿐인데 HTTPS는 어떻게 HTTP보다 Secure 한 통신을 보장한다는 걸까요?
HTTPS는 Hyper Text Transfer Protocol Secure Socket layer의 약자입니다. HTTP over SSL(TLS), HTTP over Secure라고 부르기도 합니다. HTTPS는 HTTP 요청을 SSL 혹은 TLS라는 알고리즘을 이용해, HTTP 통신을 하는 과정에서 데이터를 암호화하여 전송하는 방법입니다. HTTPS는 다음을 목적으로 사용합니다.
암호화
첫 번째는 제삼자가 서버와 클라이언트가 주고받는 정보를 탈취할 수 없도록 하는 것입니다. 이를 위해 서버와 클라이언트는 서로가 합의한 방법으로 데이터를 암호화하여 주고받습니다. 따라서 중간에 제삼자에게 데이터가 탈취되더라도 그 내용을 알아볼 수 없습니다. 기존에 배웠던 HTTP는 요청 및 응답이 탈취된다면 아래와 같이 전달되는 데이터의 내용을 제삼자가 그대로 확인할 수 있습니다. 아래 사진은 데이터를 전송하는 요청을 'wireshark'라는 패킷 분석 프로그램을 이용하여 캡처한 사진입니다.
하지만 데이터를 암호화하여 전송하는 HTTPS를 사용한다면 비밀번호와 같은 중요한 데이터가 유출될 가능성이 HTTP보다 현저히 적어집니다. 아래 사진은 위 사진과 동일한 요청을 프로토콜만 HTTPS로 변경했을 때의 데이터를 캡처한 사진입니다.
보이는 것처럼 내용이 암호화되어 전송되기 때문에 정확한 키로 복호화하기 전까지는 어떤 내용인지 알 수 없습니다.
HTTPS에서는 클라이언트와 서버가 데이터를 암호화하여 주고받기 위해 비대칭키 방식과 대칭키 방식을 혼용하여 사용합니다.
서버와 클라이언트가 통신할 때, 대칭키 방식은 양쪽이 공통의 비밀 키를 공유하여 데이터를 암호화 및 복호화하는 것, 비대칭키 방식은 각각 공개키와 비밀키(개인키)를 가지고 상대가 나의 공개키로 암호화한 데이터를 개인이 가진 비밀키로 복호화하는 것을 의미합니다.
클라이언트와 서버가 데이터를 주고받을 때는 대칭키를 사용합니다. 비대칭키 알고리즘은 대칭키 알고리즘보다 훨씬 복잡하기 때문에 대칭키를 사용하여 데이터를 암호화 및 복호화하는 것이 훨씬 컴퓨터에 부담을 덜 주기 때문입니다.
그런데 대칭키를 서로 주고받는 과정에서부터 정보가 탈취된다면 어떻게 될까요? 아마 암호화를 하든 안 하든 결국 모든 데이터가 복호화 가능하게 될 것입니다.
그래서 HTTPS는 이러한 대칭키를 주고받을 때는 비대칭키 방식으로 주고받도록 합니다. 앞서 말했듯 비대칭키는 공개키로 암호화한 정보는 개인이 가진 비밀키로만 풀 수 있으므로 중간에 대칭키가 탈취되더라도 개인키가 없이는 이를 복호화할 수 없기 때문입니다.
아직 어려우신가요? 괜찮습니다. 사실상 개발자가 코드로 해당 과정을 일일이 구현하지 않아도 브라우저와 서버가 해당 과정을 대신 처리해 주기 때문에 지금은 “HTTPS는 대칭키와 비대칭키 방식을 이용해 데이터를 암호화한다.”라는 개념만 대략 이해해도 충분합니다.
인증서
HTTPS의 또 다른 특징 중 하나는 브라우저가 서버의 응답과 함께 전달된 인증서를 확인할 수 있다는 점입니다. 이러한 인증서는 서버의 신원을 보증하여 우리가 접속한 Naver가 해커가 정교하게 따라 한 가짜 Naver가 아님을 보장해 주는 역할을 합니다.
이때 이를 보증할 수 있는 제삼자를 Certificate Authority, CA라고 부릅니다. CA는 인증서를 발급해 주는 엄격하게 공인된 기관들을 말합니다. 이러한 CA들은 서버의 공개키와 정보를 CA의 비밀키로 암호화하여 인증서를 발급합니다. (이 비밀키가 해커에게 유출되어 파산한 CA도 있습니다.)
위 웹사이트의 인증서를 살펴보겠습니다. 인증서를 발급한 CA, 서명에 사용한 알고리즘, 서명, 공개키 정보 등이 들어있는 것을 확인할 수 있습니다. 각각이 어떤 것을 뜻하는지 지금은 몰라도 됩니다. “인증서가 CA의 비밀키로 암호화되어 있으므로 CA의 공개키로 복호화 가능하다. 따라서 해당 CA에서 발급한 인증서라는 것을 보증할 수 있다.”라고만 이해해도 충분합니다. 대략적인 과정은 아래와 같습니다.
서버가 클라이언트에게 CA에서 발급받은 인증서를 전달하면 클라이언트는 OS 또는 브라우저에 미리 내장되어 있던 CA 리스트를 통해 브라우저에서 인증된 CA에서 발급받은 인증서인지 먼저 확인합니다. 만약 인증된 CA에서 발급한 인증서가 아니라면 아래와 같이 화면에 경고창을 띄워 서버와 연결이 안전하지 않다는 화면을 보여줍니다.
그 후 인증서가 확인되었다면 브라우저에 제공된 해당 CA 기관의 공개키로 서버 인증서를 복호화합니다. 앞서 말했듯 CA의 비밀키로 인증서를 암호화하였기 때문에 CA의 공개키로 복호화가 가능하겠죠? 이렇게 서명을 복호화해 얻은 공개키로 클라이언트는 서버를 믿을만한 대상인지 신뢰할 수 있습니다. 만약 인증서가 위조되었다면 CA의 공개키로 서버의 인증서를 복호화할 수 없을 테니까요!
따라서 브라우저는 인증서의 도메인과 데이터를 제공하는 서버의 도메인을 비교할 수 있으므로 '중간자 공격'을 감지하여 보안 위협으로부터 사용자 및 사용자의 데이터를 보호할 수 있습니다. 또한 위와 같은 경고를 직접 보여줌으로써 브라우저들은 인증된 CA가 발급한 인증서를 이용하여 데이터를 제공하는 안전한 서버를 사용자가 사용하도록 유도합니다.
이렇게 서버와 클라이언트 간의 CA를 통해 서버를 인증하는 과정과 데이터를 암호화하는 과정을 아우른 프로토콜을 TLS 또는 SSL이라고 말합니다. (*SSL과 TLS는 사실상 동일한 규약을 뜻하며 SSL이 표준화되며 바뀐 이름이 TLS입니다.) SSL/TLS
참고자료
[실습] 인증서 발급 및 HTTPS 서버 구현
Before You Learn
- HTTP에 대한 이해
Achievement Goals
- HTTPS의 개념을 이해할 수 있다.
- HTTPS가 왜 인증에서 필요하고, 왜 사용해야 하는지 이해할 수 있다.
Bare minimum requirement
- 로컬 환경(localhost)에서 인증서를 생성하고, 인증서를 이용해 HTTPS 서버를 실행합니다.
Getting Started: HTTPS 사설 인증서 발급 및 서버 구현
이번 시간에는 아래 설명을 참고하여 HTTPS를 학습하고 서버를 직접 구현합니다.
Spring Initializr에서 기본 웹 서버 프로젝트를 생성 후 실습을 진행하세요.
- (필수) Spring Web
자바는 다음과 같은 두 가지의 인증서 형식을 지원합니다.
- PKCS12 (Public Key Cryptographic Standards #12) : 여러 인증서와 키를 포함할 수 있으며, 암호로 보호된 형식입니다. 업계에서 널리 사용됩니다.
- JKS (Java KeyStore) : PKCS12와 유사합니다. 독점 형식이며 Java 환경으로 제한됩니다.
설치
mkcert라는 프로그램을 이용해서 로컬 환경(내 컴퓨터)에서 신뢰할 수 있는 PKCS12 형식의 인증서를 만들 수 있습니다.
단, mkcert를 이용해 생성한 인증서는 로컬 환경에서만 인증 가능한 사설 인증서기 때문에 배포 환경에서는 사용할 수 없습니다.
Windows 학습자와 Ubuntu 학습자
Windows 사용자의 경우 WSL 터미널에 다음 명령어를 이용해 설치합니다. 설치 시 명령어 실행 위치를 확인한 후 설치합니다.
- *mnt/c/Windows/system32*의 경우 윈도우 운영체제에서 사용하는 디렉토리이므로 아래의 안내처럼 홈 혹은 프로젝트 저장위치로 변경하여 진행하시기를 바랍니다.
$ cd ~ # 홈에서 아래 단계를 진행한 뒤 mv 명령어로 인증서 파일을 이동시킬 수 있습니다.
혹은
$ cd {프로젝트 저장 위치} # /mnt로 이동하면 윈도우 파일 시스템에 접근할 수 있습니다.
-------------------------
$ sudo apt install libnss3-tools
$ wget -O mkcert <https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64>
$ chmod +x mkcert
$ sudo cp mkcert /usr/local/bin/
주의
설치가 안 되는 경우 다음 명령어를 실행한 후 다시 설치를 진행해 주세요.
$ sudo apt update
MacOS 학습자
MacOS 사용자의 경우, Homebrew를 통해 mkcert를 설치할 수 있습니다.
$ brew install mkcert
# firefox를 사용할 경우 필요에 따라 설치해주세요.
$ brew install nss
인증서 생성
먼저 다음 명령어를 통해 로컬을 인증된 발급기관으로 추가해야 합니다.
$ mkcert -install
Mac OS 유저의 경우 sudo로 실행될 수 있습니다. 이 경우엔 맥북 비밀번호를 입력하시기 바랍니다. 맥북의 OS 버전에 따라 mkcert 실행될 때 맥북 키체인에서 이를 인식하여 sudo로 실행될 수 있습니다.
CA가 생성되고 설치되면 PKCS12 형식 인증서를 생성할 수 있습니다.
PKCS12 인증서를 생성하기 위해 다음 명령을 사용합니다.
$ mkcert -pkcs12 localhost
이제 옵션으로 추가한 localhost에서 사용할 수 있는 인증서가 완성되었습니다. 해당 커맨드를 입력한 위치에 localhost.p12라는 파일이 생성된 것을 확인할 수 있습니다.
여기서 발급받은 인증서를 앞으로 진행할 스프린트에서 계속 활용하게 됩니다. 저장 경로를 반드시 확인하세요.
HTTPS 서버 작성
Spring Boot를 이용하면 HTTPS 서버를 간단하게 작성할 수 있습니다.
- 생성된 인증서를 resources 폴더로 이동합니다.
!https://s3.ap-northeast-2.amazonaws.com/urclass-images/YbgEYrZq5-1629628880435.png
- application.properties or application.yml에서 관련 설정을 추가합니다.
server.ssl.key-store=classpath:localhost.p12 # -> 인증서 경로를 적습니다.
server.ssl.key-store-type=PKCS12 # -> 인증서 형식을 적습니다.
server.ssl.key-store-password=changeit # -> 인증서 비밀번호를 적습니다.
# 여기서 비밀번호인 changeit은 비밀번호를 설정하지 않았을 때의 기본값입니다.
# 인증서 비밀번호는 인증서를 생성할 때 설정하거나 생성 후 변경해줄 수 있습니다.
작성 완료 후 서버를 실행해 보면 다음과 같이 HTTPS 서버가 잘 작동하는 것을 확인하실 수 있습니다.
!https://s3.ap-northeast-2.amazonaws.com/urclass-images/CDGPS70ewiFTw0ofPyl5y-1657248289601.png
설치 과정 및 이후 과정을 모두 진행했지만 인증서 파일이 존재하지 않는 경우, 윈도우 파일 탐색기로 해당 위치로 이동하여 인증서 가져오기 마법사를 실행합니다. 마법사 실행 후 인증서 파일을 콘텐츠에 안내된 경로로 이동하여 다시 시도하시기를 바랍니다.
Hashing
해싱 (Hashing)
가장 많이 쓰이는 암호화 방식 중에 하나인 해싱을 소개합니다. 복호화가 가능한 다른 암호화 방식들과 달리, 해싱은 암호화만 가능합니다.
해싱은 해시 함수(Hash Function)를 사용하여 암호화를 진행하는데, 해시 함수는 다음과 같은 특징을 가집니다.
- 항상 같은 길이의 문자열을 리턴합니다.
- 서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나옵니다.
- 동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나옵니다.
아래 표는 대표적인 해시 함수중 하나인 SHA1에 특정 입력 값을 넣었을 때 어떤 결과가 리턴되는지 보여주는 예시입니다. 이 링크에서 SHA1 함수를 직접 사용해 볼 수도 있습니다.
레인보우 테이블과 솔트(Salt)
그런데 항상 같은 결과값이 나온다는 특성을 이용해 해시 함수를 거치기 이전의 값을 알아낼 수 있도록 기록해 놓은 표인 레인보우 테이블이 존재합니다. 레인보우 테이블에 기록된 값의 경우에는 유출이 되었을 때 해싱을 했더라도 해싱 이전의 값을 알아낼 수 있으므로 보안상 위협이 될 수 있습니다.
[사진] 레인보우 테이블 예시
이때 활용할 수 있는 것이 솔트(Salt)입니다. 솔트는 소금이라는 뜻으로, 말 그대로 소금을 치듯 해싱 이전 값에 임의의 값을 더해 데이터가 유출되더라도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법입니다.
솔트를 사용하게 되면 해싱 값이 유출되더라도, 솔트가 함께 유출된 것이 아니라면 암호화 이전의 값을 알아내는 것은 불가능에 가깝습니다.
해싱의 목적
그런데, 왜 복호화가 불가능한 암호화 방식을 사용하는 걸까요? 바로 해싱의 목적은 데이터 그 자체를 사용하는 것이 아니라, 동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이기 때문입니다.
예시를 들어보겠습니다. 사이트 관리자는 사용자의 비밀번호를 알고 있을 필요가 없습니다. 오히려 사용자들의 비밀번호를 알고 있다면, 이를 얼마든지 악용할 수 있기 때문에 심각한 문제가 생길 수도 있습니다. 그래서 보통 비밀번호를 데이터베이스에 저장할 때, 복호화가 불가능하도록 해싱하여 저장하게 됩니다. 해싱은 복호화가 불가능하므로 사이트 관리자도 정확한 비밀번호를 알 수 없게 되죠.
그럼 서버 측에서 비밀번호를 모르는 상태에서 어떻게 로그인 요청을 처리할 수 있는 걸까요? 방법은 간단합니다. 해싱한 값끼리 비교해서 일치하는지 확인하는 것이죠. 꼭 정확한 값을 몰라도, 해싱한 값이 일치한다면 정확한 비밀번호를 입력했다는 뜻이 되기 때문에, 해싱 값으로만 로그인 요청을 처리하는 데에도 전혀 문제가 없습니다.
이처럼 해싱은 민감한 데이터를 다루어야 하는 상황에서 데이터 유출의 위험성은 줄이면서 데이터의 유효성을 검증하기 위해서 사용되는 단방향 암호화 방식입니다.
[실습] 레인보우 테이블로 비밀번호 알아내기
비밀번호를 해싱할 때 salt를 설정하지 않으면 어떤 일이 생길 수 있을까요? 실습을 통해서 알아봅시다.
🧐 여러분은 해커입니다. 오늘은 몇몇 사용자의 아이디와 해싱된 비밀번호를 알아내는 데 성공했습니다. 레인보우 테이블을 활용하여 로그인에 성공하고, 정보를 빼내세요!
[ 알아낸 정보 리스트 ]
[ 레인보우 테이블 ]
[ 로그인 화면 ]
이번 실습을 통해 알 수 있듯이, 비밀번호를 해싱할 때 salt를 사용하지 않으면 Rainbow Table을 통해서 비밀번호를 파악할 수 있는 위험성이 증가합니다. 실습을 통해 해싱과 salt의 필요성을 다시 한번 생각해 보시기 바랍니다.
Chapter - Cookie
쿠키는 서버에서 클라이언트에 데이터를 저장하는 방법 중 하나입니다. 그러므로 서버가 원한다면 서버는 클라이언트에서 쿠키를 이용하여 데이터를 가져올 수 있습니다. 그러므로 쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고 클라이언트에서 서버로 쿠키를 전송하는 것도 포함됩니다.
이런 쿠키에는 다음과 같은 특징이 있습니다.
서버가 클라이언트에 데이터를 저장할 수 있습니다.
앞서 언급한 것처럼 서버는 쿠키를 이용하여 데이터를 저장하고 원할 때 이 데이터를 다시 불러와 사용할 수 있습니다. 하지만 데이터를 저장한 이후 아무 때나 데이터를 가져올 수 없습니다. 데이터를 저장한 이후 특정 조건들이 만족하는 경우에만 다시 가져올 수 있습니다.
이런 조건들은 쿠키 옵션으로 표현할 수 있습니다. 아래에서는 주로 사용되는 쿠키 옵션을 설명합니다.
1. Domain
도메인이라는 것은 여러분들이 흔하게 보실 수 있는 www.google.com과 같은 서버에 접속할 수 있는 이름입니다. 쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않습니다.
여기서 서브 도메인이란 www 같은 도메인 앞에 추가로 작성되는 부분을 말합니다. 따라서 요청해야 할 URL이 http://www.localhost.com:3000/users/login이라 하면, 여기에서 Domain은 localhost.com이 됩니다.
만약 쿠키 옵션에서 도메인 정보가 존재한다면 클라이언트에서는 쿠키의 도메인 옵션과 서버의 도메인이 일치해야만 쿠키를 전송할 수 있습니다. 이를 통해 naver.com에서 받은 쿠키를 google.com에 전송하는 일을 막을 수 있습니다.
Path
세부 경로는 서버가 라우팅 할 때 사용하는 경로입니다. 만약 요청해야 하는 URL이 http://www.localhost.com:3000/users/login인 경우라면 여기에서 Path, 세부 경로는 /users/login이 됩니다. 명시하지 않으면 기본으로 / 으로 설정되어 있습니다.
Path 옵션의 특징은 설정된 path를 전부 만족하는 경우 요청하는 Path가 추가로 더 존재하더라도 쿠키를 서버에 전송할 수 있습니다. 즉 Path가 /users로 설정되어 있고, 요청하는 세부 경로가 /users/spring인 경우라면 쿠키 전송이 가능합니다.
하지만 /posts/spring로 전송되는 요청은 Path 옵션(/users)을 만족하지 못하기 때문에 서버로 쿠키를 전송할 수 없습니다.
MaxAge or Expires
쿠키가 유효한 기간을 정하는 옵션입니다. 만약 쿠키가 영원히 남아있다면 그만큼 탈취되기도 쉬워지기 때문에 이러한 유효기간을 설정하는 것이 보안 측면에서 중요합니다. MaxAge는 앞으로 몇 초 동안 쿠키가 유효한지 설정하는 옵션입니다.
Expires 은 MaxAge와 비슷합니다. 다만 언제까지 유효한지 Date를 지정합니다. 이때 클라이언트의 시간을 기준으로 합니다. 이후 지정된 시간, 날짜를 초과하게 되면 쿠키는 자동으로 파괴됩니다.
쿠키는 위 옵션의 여부에 따라 세션 쿠키(Session Cookie)와 영속성 쿠키(Persistent Cookie)로 나눠집니다.
- 세션 쿠키: MaxAge 또는 Expires 옵션이 없는 쿠키로, 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키입니다. 브라우저를 종료하면 해당 쿠키는 삭제됩니다.
- 영속성 쿠키: 브라우저의 종료 여부와 상관없이 MaxAge 또는 Expires에 지정된 유효시간만큼 사용가능한 쿠키입니다.
4. Secure
쿠키를 전송해야 할 때 사용하는 프로토콜에 따른 쿠키 전송 여부를 결정합니다. 만약 해당 옵션이 true로 설정된 경우, 'HTTPS' 프로토콜을 이용하여 통신하는 경우에만 쿠키를 전송할 수 있습니다.
Secure 옵션이 없다면 프로토콜에 상관없이 http://www.naver.com 또는 https://www.naver.com에 모두 쿠키를 전송할 수 있습니다.
5. HttpOnly
자바스크립트에서 브라우저의 쿠키에 접근 여부를 결정합니다. 만약 해당 옵션이 true로 설정된 경우, 자바스크립트에서는 쿠키에 접근이 불가합니다.
명시되지 않는 경우 기본으로 false로 지정되어 있습니다. 만약 이 옵션이 false인 경우 자바스크립트에서 쿠키에 접근이 가능하므로 'XSS' 공격에 취약합니다.
6. SameSite
Cross-Site 요청을 받은 경우, 요청에서 사용한 메서드(e.g. GET, POST, PUT, PATCH …)와 해당 옵션의 조합을 기준으로 서버의 쿠키 전송 여부를 결정하게 됩니다. 이때, Cross-Origin과 Cross-Site를 혼동하지 않도록 주의해야 합니다.
- Cross-Origin : 서버의 도메인, 프로토콜, 포트 중 하나라도 다른 경우 Cross-Origin으로 구분됩니다.
- http://naver.com vs https://naver.com ⇒ 프로토콜이 다르므로 Cross-Origin입니다.
- https://naver.com:443 vs https://naver.com ⇒ https의 기본 포트는 443입니다. 따라서 도메인, 프로토콜, 포트가 모두 같은 Same-Origin입니다.
- Cross-Site : eTLD+1이 다른 경우 Cross-Site로 구분됩니다. 여기서 eTLD+1 이란, .com, .org과 같이 도메인의 가장 마지막 부분을 TLD(Top Level Domain, 최상위 도메인)라고 하는데, 이 최상위 도메인의 바로 왼쪽의 하위 레벨 도메인을 합한 것을 eTLD+1 이라고 합니다. 참고로, 요즘 자주 볼 수 있는 .io의 경우 바로 왼쪽의 주소를 하나 더 합한 것을 TLD라고 판단합니다.
- http://naver.com vs https://naver.com ⇒ 두 주소 모두 TLD는 .com, eTLD+1은 naver.com으로 같으므로 Same-Site입니다.
- https://code.github.io vs https://spring.github.io ⇒ 두 주소 모두 TLD는 .io, eTLD는 github.io이며 , eTLD+1은 각각 code.github.io, spring.github.io로 다르므로 Cross-Site입니다.
SameSite 옵션에서 사용할 수 있는 속성은 다음과 같습니다.
- Lax: Cross-Site 요청이라면 GET 메서드에 대해서만 쿠키를 전송할 수 있습니다.
- Strict: 단어 그대로 가장 엄격한 옵션으로, Cross-Site가 아닌 Same-Site인 경우에만 쿠키를 전송할 수 있습니다.
- None: Cross-Site에 대해 가장 관대한 옵션으로 항상 쿠키를 보내줄 수 있습니다. 다만 쿠키 옵션 중 Secure 옵션이 필요합니다.
쿠키를 이용한 상태 유지
이러한 쿠키의 특성을 이용하여 서버는 클라이언트에 인증정보를 담은 쿠키를 전송하고, 클라이언트는 전달받은 쿠키를 요청과 같이 전송하여 Stateless 한 인터넷 연결을 Stateful 하게 유지할 수 있습니다.
하지만 기본적으로는 쿠키는 오랜 시간 동안 유지될 수 있고, 자바스크립트를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험합니다.
이런 인증정보를 탈취하여 서버에 요청을 보낸다면 서버는 누가 요청을 보낸 건지 상관하지 않고 인증된 유저의 요청으로 취급하기 때문에, 개인 유저 정보 같은 민감한 정보에 접근이 가능합니다.
심화 학습
- 쿠키 옵션 중 하나인 same-site에 대한 정확한 이해를 위해 same-origin과의 차이에 대하여 알아보세요.
- 쿠키를 탈취한 후 악용할 수 있는 대표적인 보안 공격에 대해 궁금하시다면 아래 링크를 참고해 주세요.
- 위키피디아 - XSS(사이트 간 스크립팅)
- [위키피디아 - CSRF(사이트 간 요청 위조)] (https://ko.wikipedia.org/wiki/사이트_간_요청_위조)
참고 자료
Chapter - Session
세션기반 인증 (Session-based Authentication)
로그인
!https://s3.ap-northeast-2.amazonaws.com/urclass-images/_PsLhwyFA-1618305417398.png
로그인을 통해 인증 정보가 저장되고, 인증된 사용자가 어떤 식으로 웹사이트를 이용하는지 간단하게 알아봅시다.
사용자가 웹사이트에서 아이디 및 비밀번호를 이용해서 로그인을 시도하면(그림 1), 과연 어떤 일이 벌어질까요?
사용자가 만일 정확한 아이디와 비밀번호를 입력했다면, 서버는 인증(Authentication)에 성공했다고 판단할 것입니다. 그렇다면, 다음번에 인증을 필요로 하는 작업(그림에서와 같이, 장바구니에 물품 추가)을 요청할 경우, 또 로그인 과정을 거쳐야 할까요?
아닙니다. 서버가 "해당 유저는 인증에 성공했음"을 알고 있다면, 유저가 매번 로그인할 필요가 없을 것입니다.
인증에 따라 리소스의 접근 권한(Authorization) 이 달라집니다.
이때 서버와 클라이언트에 각각 필요한 것이 다음과 같습니다.
- 서버는 사용자가 인증에 성공했음을 알고 있어야 합니다.
- 클라이언트는 인증 성공을 증명할 수단을 갖고 있어야 합니다.
여기서 몇 가지 용어가 등장합니다.
- 사용자가 인증에 성공한 상태는 세션이라고 부릅니다.
- 서버는 일종의 저장소에 세션을 저장합니다. (그림에서 2번) 주로 in-memory, 또는 세션 스토어(redis 등과 같은 트랜잭션이 빠른 DB)에 저장합니다.
- 세션이 만들어지면, 각 세션을 구분할 수 있는 세션 아이디도 만들어지는데(그림에서 3번), 보통 클라이언트에 세션 성공을 증명할 수단으로써 세션 아이디를 전달합니다. (그림에서 4번)
이때 웹사이트에서 로그인을 유지하기 위한 수단으로 쿠키를 사용합니다. 쿠키에는 서버에서 발급한 세션 아이디를 저장합니다
쿠키를 통해 유효한 세션 아이디가 서버에 전달되고, (그림에서 5번) 세션 스토어에 해당 세션이 존재한다면 (그림에서 6번) 서버는 해당 요청에 접근 가능하다고 판단합니다. (그림에서 7,8번)
하지만 쿠키에 세션 아이디 정보가 없는 경우, 서버는 해당 요청이 인증되지 않았음을 알려줍니다.
로그아웃
그렇다면, 로그아웃은 어떻게 구현해야 할까요?
세션 아이디가 담긴 쿠키는 클라이언트에 저장되어 있으며, 서버는 세션을 저장하고 있습니다. 그리고 서버는 그저 세션 아이디로만 인증 여부를 판단합니다.
주의: 쿠키는 세션 아이디, 즉 인증 성공에 대해 증명을 하고 있으므로, 탈취될 때 서버는 해당 요청이 인증된 사용자의 요청이라고 판단합니다. 이것이, 우리가 공공 PC에서 로그아웃해야 하는 이유입니다.
그러므로 로그아웃은 다음 두 가지 작업을 해야 합니다.
- 서버 : 세션 정보를 삭제해야 합니다.
- 클라이언트 : 쿠키를 갱신해야 합니다.
서버가 클라이언트의 쿠키를 임의로 삭제할 수는 없습니다. 대신, set-cookie로 클라이언트에게 쿠키를 전송할 때 세션 아이디의 키값을 무효한 값으로 갱신할 수 있습니다.
SQL Injection
SQL Injection(SQL 삽입)은 웹 해킹을 접한다면 가장 먼저 배우는 공격 기법인 만큼 간단하지만 아주 강력한 공격입니다. 이름처럼 데이터베이스에서 임의의 SQL 문을 실행할 수 있도록 명령어를 삽입하는 공격 유형입니다. 응용 프로그램의 보안상의 허점을 이용해 데이터베이스를 비정상적으로 조작하며, 이에 따라 기록이 삭제되거나 데이터가 유출될 수 있습니다.
공격 시나리오를 통해 자세히 알아봅시다.
- SQL 삽입 공격은 보통 사용자가 input form에 직접 무언가 작성하는 상황에서 발생합니다. 예를 들어 로그인 상황을 생각해 보면 웹 사이트에 로그인할 때, 입력한 아이디 값과 패스워드값을 이용해 바로 데이터베이스에 접근합니다. 만약 클라이언트가 kimcoding이라는 아이디 값을 보낸다면 대략적인 코드는 다음과 같을 것입니다. (지금 해당 SQL에 대해 전부 이해할 필요는 없습니다. 클라이언트가 넘겨준 정보를 그대로 SQL에 삽입한다는 점에 주목하세요!)
- SELECT * FROM users WHERE auth='admin' AND id='kimcoding';
- 공격자는 input form에 일반 텍스트(아이디 및 패스워드)가 아닌 SQL 문을 작성합니다. 입력받은 아이디와 패스워드를 통해 데이터베이스를 조회하는데, 패스워드에 ’OR ‘1’ = ‘1을 넣어 보낸다면 다음과 같은 SQL 문이 완성됩니다.
- SELECT * FROM users WHERE auth='admin' AND id='' OR '1'='1';
- WHERE 절에서 OR는 AND보다 연산 순위가 낮으므로 OR 절인 ‘1’ = ‘1’ (항상 참)이 가장 나중에 실행되어 결국 로그인에 성공합니다. 혹은 아래의 코드처럼 input form에 SQL 문을 마무리하는 키워드인 ;와 함께 주요 테이블을 삭제하는 SQL 문(e.g. '; DROP TABLES users;--')을 작성한다면 데이터가 모두 삭제되는 큰 피해를 볼 수도 있습니다.
- SELECT * FROM users WHERE auth='admin' AND id='';DROP TABLES users;--';
SQL injection 대응 방안
- 입력(요청) 값 검증보안에서 화이트리스트란 기본 정책이 모두 차단인 상황에서 예외적으로 접근이 가능한 대상을 지정하는 방식 또는 그 지정된 대상들을 말합니다.
- SQL 문은 사람이 사용하는 자연어와 비슷하므로 키워드를 막기엔 한계가 있습니다. 따라서 블랙리스트가 아닌 화이트리스트 방식으로 해당 키워드가 들어오면 다른 값으로 치환하여 SQL Injection에 대응할 수 있습니다.
- Prepared Statement 구문 사용
- Prepared Statement 구문을 사용하면 사용자의 입력이 SQL 문으로부터 분리되어 SQL Injection을 방어할 수 있습니다. 사용자의 입력값이 전달되기 전에 데이터베이스가 미리 컴파일하여 SQL을 바로 실행하지 않고 대기하며, 사용자의 입력값을 단순 텍스트로 인식합니다. 따라서 입력값이 SQL 문이 아닌 단순 텍스트로 적용되며 공격에 실패하게 됩니다.
- Error Message 노출 금지
- 공격자는 데이터베이스의 Error Message를 통해 테이블이나 열 등 데이터베이스의 정보를 얻을 수 있습니다. 에러가 발생한 SQL 문과 에러 내용이 클라이언트에 노출되지 않도록 별도의 에러핸들링이 필요합니다.