인증/보안 – 기초 1
HTTPS
HTTP + Secure
→ HTTP 프로토콜 내용을 암호화(보안성)
HTTP 요청을 SSL 혹은 TLS 알고리즘을 이용해서 내용을 암호화
TLS는 SSL의 진화버전?이라고 보면 되는데, 통상 같이 부른다
// HTTPS의 특징
* 인증서
* CA
* 비대칭키 암호화
인증서(Certificate)
데이터 제공한 서버가 정말로 데이터를 보낸 서버가 맞는지 확인하는 용도
- 데이터 제공자 신원 보장
- 도메인 종속 - 도메인 정보가 들어가 있음
기본 로직은 다음과 같다
1. Client가 인증서(domain 정보 포함)와 함께 서버에 요청
2. 서버는 인증서와 함께 응답을 전송
3. Client는 인증서에 작성된 domain과 응답(res) 객체에 작성된 domain을 비교
→ domain이 같다면 정보를 보낸 서버가 맞다는 것
→ 공격자의 제3자 공격 등 중간에서 가로채서 변조하는 행위(중간자 공격)에 대한 판별 가능
CA(Certificate Autority)
공인인증서 발급 기관
각 브라우저는 신뢰하는 CA를 가지고 있다(브라우저마다 CA가 다를 수 있다는 것)
→ 브라우저들은 응답을 받을 시에 인증된 CA가 발급한 인증서가 아니라면 경고 창을 띄움(서버와 연결이 안전하지 않다는 경고)
→ 인증된 CA가 발급한 인증서를 이용하여 데이터를 제공하는 안전한 서버를 사용할 수 있게 사용자를 유도함
비대칭키 암호화
서로 다른 키 한 쌍으로 암호화를 진행(암호화 키, 복호화 키)
→ 공개키, 비밀키
모든 통신에 대해서 공개키를 매번 사용하진 않는다
처음 통신을 맺을 때, 비밀키를 생성하기 위해서 공개키를 사용
-
- Hand Shake 구간
Client와 Server간 Hand Shake 시에 , 서로 hello 패킷을 통해 서로를 확인하고 서버는 클라이언트에 공개키 하나를 전달
- Hand Shake 구간
-
- 비밀 키 생성 구간
클라이언트는 전달받은 키를 이용해서 서로와 키를 만들어 낼 임의의 정보를 암호화해서 전송.
서버도 마찬가지로 암호화해서 클라이언트에 전송(각 서로에게 키 제작용 랜덤 string을 전송)
→ 서로 교환한 정보를 바탕으로 비밀키를 생성
- 비밀 키 생성 구간
- 상호 키 검증 구간
클라이언트는 테스트용 데이터를 비밀 키로 암호화해서 전달
서버는 받은 데이터를 복호화 후 다시 암호화를 해서 전달
→ 클라이언트가 다시 그 데이터를 받아서 복호화에 성공하면상호키 검증이 되었다는 것
(HTTPS 연결 성립)
사설 인증서 발급 및 HTTPS 서버 구현
mkcert 프로그램을 통해서 로컬 환경에 신뢰할 수 있는 인증서 생성이 가능
mkcert 설치
// 우분투
$ 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/
인증서 생성
로컬을 인증된 발급기관으로 추가
$ mkcert -install
로컬 환경에 대한 인증서 생성
// localhost에 대한 인증서를 만드는 명령어
$ mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
옵션으로 추가한 localhost, 127.0.0.1(IPv4), ::1(IPv6)에서 사용할 수 있는 인증서가 완성되었고 cert.pem, key.pem 이라는 파일이 생성된 것을 확인 가능
→ 발급받은 key와 cert의 저장 경로를 반드시 확인할 것!
→ 반드시 해당 파일이 존재하는 경로에 잘 매핑시켜줘야한다
인증서(cert.pem)은 공개키와 인증기관의 서명이 담긴 것이므로 공개되어도 무관key.pem 은 개인키이므로 절대 외부에 노출되면 안된다(git 하면 안됨!)
→ gitignore 에 등록해 둘것(*.pem)
HTTPS 서버 작성
node.js 환경에서 HTTPS 서버를 작성하기 위해서는 https 내장 모듈을 이용하여 작성하기(http 모듈 아님)
// 작성예시
const https = require('https');
const fs = require('fs');
https
.createServer(
{
key: fs.readFileSync(__dirname + '/key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + '/cert.pem', 'utf-8'),
},
function (req, res) {
res.write('Congrats! You made https server now :)');
res.end();
}
)
.listen(3001);
또는 express.js 에서는 다음과 같이 https.createServer의 두 번째 파라미터에 들어갈 callback 함수를 Express 미들웨어로 교체하면 된다
// 작성 예시
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
https
.createServer(
{
key: fs.readFileSync(__dirname + '/key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + '/cert.pem', 'utf-8'),
},
app.use('/', (req, res) => {
res.send('Congrats! You made https server now :)');
})
)
.listen(3001);
ngrok 프로그램을 이용한 https 터널링
http로 만들어진 서버를 https 프로토콜로 터널링해주는 프로그램
→ 기존에 작성한 http 서버를 https 프로토콜화시키는 것
Hashing(암호화 작업)
어떠한 문자열에 임의의 연산을 적용하여 다른 문자열로 변환하는 것
Hash 알고리즘의 3가지 원칙
* 모든 값에 대해 해시 값을 계산하는데 오래걸리지 않아야 함
* 최대한 해시 값을 피해야 하며, 모든 값은 고유한 해시 값을 가진다
→ 극히 드물지만, 해시 값들이 같은 값을 가지는 경우가 존재(중복)
→ 이 중복이 적을수록 좋은 해시 알고리즘을 가진거임
* 아주 작은 단위의 변경이라도 완전히 다른 해시 값을 가져야 한다
3티어 아키텍쳐에서 비밀번호를 해시 값으로 가지고 있으면 데이터가 털려도 그나마 안전할 수 있다는 것
Salt
암호화해야 하는 값에 어떤 별도의 값을 추가하여 결과를 변형하는 것
(마치 소금치듯이?)
암호화만 해둔다면 해시된 결과가 늘 동일
→ 해시된 값과 원본값을 비교한 테이블(레인보우 테이블)을 만들어서 디코딩해서 암호를 푸는 문제가 발생할 수 있다
원본 값에 임의로 약속된 별도의 문자열을 추가하여 해시를 진행
→ 기존 해시값과 전혀 다른 해시값이 반환되어 알고리즘이 노출되더라도 원본 값을 보호할 수 있는 안전 장치
* 기존: (암호화 하려는 값) => (hash 값)
→ 암호화하려는 값을 바로 hash 했었음
* salt 사용: (암호화 하려는 값) + (salt 용 값) => (hash 값)
→ 암호화하려는 값에 salt용 값을 추가한 후 hash 진행
salt 사용 시 주의점
* salt는 유저와 패스워드 별로 유일한 값을 가져야 함
* 사용자 계정을 생성할 때와 비밀번호를 변경할 때 마다 새로운 임의의 salt를 사용해서 hashing 해야함
* salt는 절대 재사용 x
* salt는 DB의 유저 테이블에 같이 저장되어야 한다
Cookie(쿠키)
HTTP 요청은 stateless(무상태성) 해서 각각의 요청은 독립적인데 어떻게 우리의 정보가 유지될까? (ex. 쇼핑몰 장바구니) → 쿠키(무상태성을 보완해주는 도구)
어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터
서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단
→ 해당 도메인에 대해 쿠키가 존재하면, 웹 브라우저는 도메인에게 http 요청 시 쿠키를 함께 전달
→ 장시간 보존해야 하는 정보를 클라이언트에 저장할 때 적합(장바구니 정보, 사용자 선호, 테마, 로그인 상태 유지, 로그인 로그아웃의 인증 정보, 회사가 필요한 마케팅 정보 등, etc)
쿠키를 이용한다는 것은 서버→ 클라이언트로 쿠키 전송뿐만 아니라, 클라이언트→ 서버로 쿠키 전송하는 것도 포함하는 뜻
쿠키 전달 방법
1. 서버가 응답 헤더에 `set-Cookie` 프로퍼티에 쿠키 정보(쿠키 이름, 값, 경로 등 옵션)를 담아서 클라이언트에 전달
2. 클라이언트는 응답 헤더의 set-Cookie를 확인하게 됨
3. 다음 요청 시 마다 클라이언트는 쿠키의 이름과 값을 서버에 전달하게 됨
서버가 클라이언트에 데이터를 저장한 후에 아무 때나 마음대로 가져올 수 있는 것은 아니다
특정 조건들을 만족하는 경우(쿠키 옵션)에만 다시 가져올 수 있다
쿠키의 옵션
Domain
서버와 요청의 도메인이 일치하는 경우 쿠키 전송
여기서의 도메인은 포트, 서브 도메인 정보, 세부 경로는 포함하지 않는다
//ex naver.com 같은거
URL: http://www.localhost.com:3000/users/login
→ localhost.com
Path
서버와 요청의 세부 경로가 일치하는 경우 쿠키 전송
여기서의 세부 경로는 서버가 라우팅할 때 사용하는 경로
명시하지 않으면 default는 /
//ex
URL: http://www.localhost.com:3000/users/login
→ users/login
설정된 path를 전부 만족한다면 그 뒤에 path가 더 존재하더라도 쿠키 전송 가능
→ 즉 해당 path를 포함하고 있으면 된다는 거
MaxAge or Expires
쿠키의 유효기간을 정하는 옵션
→ pc방 가서 로그아웃 안 한 경우처럼 위험한 경우 존재하니깐 일정시간 후 자동 소멸하도록
MaxAge의 단위는 초
Expires의 클라이언트의 시간을 기준으로 언제까지 유효한지 Date 지정
옵션이 설정되지 않은 경우, 브라우저의 탭을 닫아야 쿠키가 제거됨
HttpOnly
자바스크립트의 쿠키 접근 가능 여부 설정(브라우저만 접근 가능하게 할지?아닐지?)
→ 쿠키는 <script> 태그로 접근 가능하므로, 스크립트 삽입 공격(xss 공격)에 취약
→ 스크립트 태그로 접근 불가하게 옵션(true)
기본값은 false(접근 가능)
Secure
프로토콜에 따른 전송 여부를 결정
→ HTTPS 프로토콜에서만 쿠키 전송 되게 하려면 true
SameSite
CORS 요청과 관련된 부분으로 해당 옵션으로 CSRF 공격을 방어할 수 있다
CORS 요청(Cross-Origin 요청)의 경우 옵션 및 메서드의 따라 쿠키 전송 여부 결정
→ CSRF(Cross Site Request Forgery; 크로스 사이트 요청 위조. 웹사이트 취약점 공격 중 하나) 공격을 보완
- Lax :Cross-Origin 요청이면
GET메소드에 대해서만 쿠키를 전송 - Strict : Cross-Origin이 아닌
same-site 인 경우에만쿠키를 전송
(same-site는 요청을 보낸 Origin과 서버의 도메인이 같은 경우를 말함) - None: 항상 쿠키를 허용. 다만 쿠키 옵션 중 Secure 옵션 설정이 필요
쿠키를 이용한 상태 유지
이러한 쿠키의 특성과 옵션을 이용하여 stateless 한 인터넷 연결을 stateful하게 유지할 수 있다
하지만, 쿠키는 오랜 시간 유지 가능하고, 자바스크립트로 접근 가능하므로 쿠키에 민감한 정보(패스워드 같은거)를 담는 것은 위험하다는 것!을 유념
Session 기반 인증
Session?
서버와 클라이언트 간 연결이 활성화된 상태(사용자가 인증에 성공한 상태)
서버가 Client에 유일하고 암호화된 ID를 부여
중요 데이터는 서버에서 관리
- 서버는 사용자가 인증에 성공했음을 알고 있어야 한다
- 클라이언트는 인증 성공을 증명할 수단을 갖고 있어야 한다
기본 로직
1. 클라이언트가 로그인을 통해 인증에 성공
2. 서버는 이 인증에 성공한 상태(세션)를 DB에 저장하고, 이렇게 세션이 만들어지면 세션 아이디도
만들어지면서 DB가 session_id를 서버에 반환
3. 서버는 클라이언트에 seesion_id를 암호화하여 Set-Cookie 헤더에 담아서 응답(res) 패킷으로 전달
→ 이 때, 웹 사이트에서 로그인을 유지하기 위한 수단으로 `쿠키`를 사용
→ 쿠키에는 서버에서 발급한 `세션 아이디를 저장`한다
4. 클라이언트는 이제 저장된 session_id로 필요한 작업을 요청하고 저장소에 해당 세션이 존재한다면
서버는 해당 요청에 접근 가능하다고 판단하여 작업을 수행 후 응답
→ 일종의 신분증? 같은 역할
로그아웃 구현
로그아웃을 구현하려면 다음 2가지 작업을 하도록 만들어야 함
* 서버의 세션 정보를 삭제해야 한다
* 클라이언트의 쿠키를 갱신해야 한다
서버가 클라이언트의 쿠키를 임의로 삭제할 수는 없다
→ 헤더의 set-cookie에서 세션 아이디의 키 값을 무효한 값으로 갱신하는 방식으로 구현 가능
쿠키 vs. 세션
Cookie
- 그저 http의 stateless한 부분(무상태성)을 보완해주는 도구
- 접속 상태 저장 경로: 클라이언트
- 장점: 서버의 부담을 경감
- 단점: 쿠키 그 자체는 인증이 아님(민감정보 보관에는 좋지 못함)
Session
- 접속 상태를 서버가 가짐(stateful)
- 접속 상태와 권한 부여를 위해 세션 아이디를 쿠키로 전송
- 접속 상태 저장 경로: 서버
- 장점: 신뢰할 수 있는 유저인지 서버에서 추가로 확인 가능(한번 더 검증 가능하므로)
- 단점:
* 하나의 서버에서만 접속 상태를 가지므로 분산에 불리
→ 서버 저장 공간에 세션 유저 데이터를 저장하고 있음(성능 저하 우려)
* 여전히 쿠키를 사용하므로 스크립트 공격(XSS)에 취약
express-session
세션을 대신 관리해 주는 미들웨어
세션 아이디를 쿠키에 저장하고, 해당 세션 아이디에 종속되는 고유한 세션 객체를 서버 메모리에 저장
세션 객체는 서로 독립적인 객체로 각각 다른 데이터를 저장할 수 있음
req.session
→ 세션 객체로서, 세션 데이터를 저장하거나 불러오기 위해 사용
→ req.session 객체에다가 키와 밸류 값을 추가하는 식으로 사용하면 된다
모듈의 역할 및 사용법은 해당 문서를 참고
https://github.com/expressjs/session#reqsession
추가 내용
인증서
let’s encrypt: 무료 ca 기관
해싱
입력받은 데이터를 고정된 길이의 데이터로 변환하여 이전의 데이터를 알아볼 수 없게 만드는것
알고리즘: sha1/sha256/sha512/base64 등이 존재
input→ output이 늘 동일한 순수 함수
단방향 암호화 함수
솔트
암호화해야 하는 원본 값에 어떤 별도의 값을 추가해서 결과를 변형
데이터베이스 사용자 테이블에 해싱된 패스워드 값과 솔트값을 함께 저장
express-session 모듈에서는 secret이라는 property 로 salt를 사용할 수 있다
ngrok
로컬에 구성한 개발 환경을 급하게 외부에 공개해야 할 경우에 사용하기 좋음
쿠키
작은 문자열을 저장
connect.sid=세션id 로 사용
서버는 일방적으로 클라이언트에 쿠키를 저장시킬 수 있다
→ 서버에서 응답을 보낼 때, 헤더를 이용(set-cookie 헤더에 키와 값 쌍을 담아서 전달)
'SE Bootcamp 내용 정리' 카테고리의 다른 글
| 인증/보안 - 기초 2: CSRF, Token (0) | 2021.11.25 |
|---|---|
| 인증/보안 - session 기반 인증 구현하기 실습 (0) | 2021.11.25 |
| 데이터베이스 - MVC 패턴 연습 (0) | 2021.11.19 |
| 데이터베이스 - MVC 패턴 기초 (0) | 2021.11.16 |
| 데이터베이스 - 관계형 데이터베이스 보충 내용 (0) | 2021.11.15 |
