1. 쿠키교환을 하려면 동일 출처 정책을 따라야 한다.
동일 출처 정책(SOP)이란?
동일 출처 정책(same-origin policy)은 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.
즉, Origin이 모두 일치해야 동일 출처로 인정된다.
- Origin이 일치하고 경로(path)만 다른 경우는 동일 출처이다.
- 프로토콜이 다르다면 동일 출처가 아니다.
- 포트가 다르다면 동일 출처가 아니다.
- 호스트가 다르다면 동일 출처가 아니다.
2. 정책에 따라 Nginx와 express의 쿠키 교환이 안되는 이유
JWT가 쿠키에 넣어 클라이언트에게 응답되는 과정
- https://test.iptime.org(443포트) 또는 http://test.iptime.org(80포트)로 요청이 들어온다.
- Nginx는 설정에 따라 index.html을 응답한다.
- index.html에 의해 화면이 랜더링 되고 유저는 로그인을 요청한다.
- 로그인에 성공하면 서버는 JWT를 발급한다.
- 그리고 발급된 JWT를 쿠키에 넣어서 브라우저에게 응답한다.
- 브라우저는 동일 출처 정책을 지키지 않았기 때문에 따라 쿠키를 거부한다.
동일 출처 정책을 지키지 않게된 이유
- Nginx는 80, 443 포트를 사용한 요청만 응답되도록 한다.
- 리버스 프록시 서버(api 서버)로 사용되는 express는 8080 포트를 사용한다.
- 그렇기 때문에 동일 출처 정책에 의해 일반적인 방법으로는 두 서버간에 쿠키를 교환하지 못한다.
3. 동일 출처로 인정되기 위한 과정 3단계
로컬에서 다른 포트를 사용하는 클라이언트와 서버간에 쿠키를 교환하기 위해서는 몇가지 설정이 필요하다.
먼저 클라이언트쪽에서는 자격증명 헤더가 포함된 자격증명 요청을 해야한다.
서버는 클라이언트가 다른 출처이기 때문에 cors orign 설정을 해야한다.
또한 서버는 클라이언트에서 보낸 자격증명 요청에 대한 응답 헤더에 쿠키를 담아 보낼 수 있는데 이때,
Access-Control-Allow-Credentials:true
라는 헤더가 응답에 포함되지 않으면 브라우저에서 해당 응답을 거부한다.
즉, 클라이언트는 자격증명 요청을 해야하고, 서버는 cors 설정과 자격증명 요청에 대응하는 Credentials:true
헤더를 응답해야 다른 포트간 쿠키를 사용한 통신이 가능하다.
- 1단계 - 로컬 환경에서 포트가 다른 서버 cookie 교환 설정
- 2단계 - Nginx와 Proxy서버 cookie 교환 설정
- 3단계 - https통신을 위한 ssl 인증서 설정
4. 1단계 - 로컬에서 포트가 다른 서버 cookie 교환 설정
로컬에서 react와 express에서 쿠키교환이 안되는 이유
- localhost에서 react는 3000번 포트를 사용한다.
- localhost에서 express는 8080번 포트를 사용한다.
- 동일 출처 정책에 따르면 포트가 다른 서버는 다른 도메인으로 인식한다.
- 때문에 express에서 쿠키를 발행하여도 브라우저에서 쿠키를 저장하지 않는다.
결론 부터 말하면 클라이언트와 서버 모두 설정이 필요하다.
- react에서는 fetch API의 자격증명(
credentials
) 옵션을 설정한다. - express에서는
cors
미들웨어의 옵션 설정이 필요하다.
- XMLHttpRequest, fetch API, axios 모두 자격증명(
credentials
) 옵션을 지원한다.
- XMLHttpRequest, fetch API, axios 모두 자격증명(
** project_bmw_front의 경우 fetch API를 사용하기 때문에 fetchAPI를 기준으로 설명한다.
4.1. 클라이언트(react): fetch API 자격증명(credentials
) 옵션 설정
const res = await fetch(`http://localhost:8080/api/auth/singup`, {
/* credentials에 'include'로 옵션 설정 */
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
Q) fetch API credentials
옵션이란?
- 기본적으로
fetch
는 쿠키를 보내거나 받지 않는다. - 때문에 fetch API를 사용해 쿠키를 전송하려면 자격증명 설정해야 한다.
- 요청에 자격증명 설정을 하려면
credentials
옵션을 사용해야 한다. - 그리고 다른 도메인간에 쿠키를 전송한다면,
credentials
옵션 값을include
로 한다.
4.1.1. fetch API 자격증명(credentials
)에서 제공하는 옵션 3가지
- 다른 Origin간 교환 설정 - 교차 출처 쿠키 교환 설정
credentials: 'include'
- 같은 Origin간 교환 설정 - 동일 출처만 쿠키 교환 설정
credentials: 'same-origin'
- 보안상의 이유나 특정이유로 자격증명을 사용하지 않을 때 사용
credentials: 'omit'
4.2. 서버(express): cors
미들웨어 자격증명 옵션 설정
app.use( cors({ origin: true, credentials: true}),);
// OR
app.use( cors({ origin: '<http://localhost:3000>', credentials: true}),);
💡 cors 미들웨어 origin
설정이 필요한 이유?
- 브라우저는 보안상의 이유로 client 스크립트에서 발생한 교차 출처 HTTP 요청을 제한한다.
(교차 출처는 동일 출처가 아닌 요청을 말한다.) - 그리고 cors(Cross-Origin Resource Sharing)는 교차 출처 요청을 할 수 있도록 하는 시스템이다.
- 일반적으로 서버에서 cors를 설정하려면,
Access-Control-Allow-Origin
헤더를 설정한다. - express에서는 일반적으로 cors 미들웨어를 사용하여
Access-Control-Allow-Origin
헤더를 설정한다. - 그리고 cors 미들웨어에서
origin
옵션 제공한다. origin
에 값을 설정하면Access-Control-Allow-Origin
에 직접 값을 설정하는 것과 같은 동작을 한다.
💡cors 미들웨어 credentials
옵션 설정이 필요한 이유?
- cors 옵션에는 자격증명 요청(credentialed requests)에 응답 헤더를 설정하는 기능이 있다.
- 위의 'fetch API credentials 옵션 설정'의 설명처럼 client는 요청에 쿠키를 담아 보낼 수 있다.
- 이것을 자격증명 요청(credentialed requests)이라고한다.
- 서버는 자격증명 요청이 들어와도 일반적인 요청과 동일하게 취급하고 응답한다.
- 하지만 서버가 자격증명 요청에 적절한 응답을 했더라도 응답 헤더에
Access-Control-Allow-Credentials: true
가 없다면, 브라우저는 해당 요청에 대한 응답을 거부한다. - 즉, 클라이언트에서 발생한 자격증명 요청을 브라우저가 거부하지 않게 하려면 응답 헤더에
Access-Control-Allow-Credentials: true
포함되어야 한다.
4.2.1. express cors 미들웨어 origin
옵션 사용방법
- 모든 교차 출처 요청을 허용 - 와일드 카드(
"*"
) 사용
cors({ origin: '*' });
// === Access-Control-Allow-Origin: "*"
- 특정 교차 출처 요청만 허용 - 직접 URL 설정
cors({ origin: '<http://localhost:3000>' });
// === Access-Control-Allow-Origin: "<http://localhost:3000>"
- 특정. 교차 출처 자동 설정 -
true
사용 - cors 미들웨어는 요청 헤더의 Origin에 맞춰 자동으로 값을 설정해주는 옵션을 지원한다.
cors({ origin: true });
// === Access-Control-Allow-Origin: "${Request Headers Origin}"
- 중요:
true
설정은 와일드 카드("*"
)와 다르다. ⇒true
는 응답 헤더에 실제 값을 부여한다.
- 중요:
cors({ origin: '*' });
cors({ origin: true });
4.2.2. cors 미들웨어 credentials
옵션 사용방법
cors({
origin: true, // === Access-Control-Allow-Origin: "<http://localhost:3000>"
credentials: true // === Access-Control-Allow-Credentials: true
})
- *중요:
credentials
옵션과origin
옵션의 와일드 카드(`""`)는 같이 사용하지 못한다.
- *중요:
Access-Control-Allow-Credential: true
헤더는 보안상의 이유로 모든 교차 출저 요청을 허용하는Access-Control-Allow-Origin: "*"
헤더와 같이 사용하지 못한다.Access-Control-Allow-Credential: true
헤더를 사용하려면,Access-Control-Allow-Origin
헤더에 구체적인 Origin이 들어가야한다.- 이러한 이유로 cors 미들웨어는
origin: true
옵션을 제공하여credentials: true
옵션와 같이 사용할 수 있게 하였다.
4.3. 크롬 개발자 모드에서 credentials
옵션 사용과 미사용 비교
4.3.1. cors에 credentials 옵션 미사용
CORS preflight 요청 결과 ⇒ Access-Control-Allow-Credentials: true 헤더 없음
브라우저 개발자 모드 CORS error 확인
CORS error 응답 내용 확인
4.3.2. cors에 credentials 옵션 사용
CORS preflight 요청 결과 ⇒ Access-Control-Allow-Credentials: true 헤더 있음
signin 응답 확인 ⇒ 쿠키도 정상적으로 응답받고 있다.
쿠키 저장 확인 ⇒ signin 요청에서 응답받은 쿠키를 브라우저가 가지고 있다.
5. 2단계 - 운영중인 Nginx와 Proxy서버 cookie 교환 설정
5.1. 1단계에서 진행한 react, express 자격증명 쿠키 교환 설정 진행
- express cors 미들웨어 origin옵션 값만 Nginx 도메인으로 설정한다.
app.use( cors({ origin: '<http://test.iptime.org>', credentials: true}),);
5.2. nginx.conf 에서 proxy 설정
# ... 생략
http {
# 1. 포워드할 upstream 프록시 서버 설정
upstream api-server {
server api:8080; # 도커서비스이름:포트
}
server {
# ... 생략
# 2. api 경로 - 프록시 설정
location /api {
proxy_pass <http://api-server>;
proxy_redirect off;
proxy_set_header Host $host;
# Frowarded 헤더 설정
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
#
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
proxy_http_version 1.1;
# 해당 옵션은 api 서버에서 처리하고 있다.
#add_header 'Access-Control-Allow-Origin' '${origin}';
#add_header 'Access-Control-Allow-Credentials' 'true';
#proxy_cookie_path /api "/; SameSite=None; HTTPOnly; Secure";
}
}
# ... 생략
}
5.3. express 프록시 설정
// express 프록시 설정
// 참고: <https://expressjs.com/ko/guide/behind-proxies.html>
app.set('trust proxy', true);
5.4. express 응답 쿠키 옵션 설정
// express 응답 객체를 사용하여 쿠키에 엑세스 토큰 저장
res.cookie(key, accessToken, {
secure: true // 쿠키를 https에서만 사용 하도록 설정
sameSite: 'none' // 모든 도메인에서 쿠키 사용 허용, *이 옵션은 https에서만 가능하다.
});
5.4.1 sameSite
옵션 설명
sameSite: 'strict'
:- 서로 다른 도메인(교차출처)에서 전송 불가능.
- 보안성은 높으나 편의가 낮다.
sameSite: 'lax'
:- 서로 다른 도메인이지만 일부 예외
- HTTP get method / a href / link href 에서는 전송 가능.
sameSite: 'none'
:- 모든 도메인에서 전송 가능
- 해당 옵션은 브라우저에 따라 다를 수 있다고 하며, 크롬의 경우 'none'을 사용한다.
- https 환경 필수: 2020년 2월 4일 릴리즈된 구글 크롬(Google Chrome) 80버전 부터는 https에서만 동작한다.
6. 3단계 - https통신을 위한 ssl 인증서 설정
6.1. 방법1) openssl 설정
openssl을 사용하는 방법은 정식 ssl 인증서를 받는 방법이 아니다. 그럼에도 이 방법을 사용하는 이유는 정식 도메인이 없기 때문이다. 개발시점에는 정식 도메인이 아닌 iptime에서 제공하는 DDNS를 사용하여 서버를 올렸다. 그리고 iptime ddns는 20년 7월 기준으로 더 이상 정식 인증서를 제공해주는 업체가 없다. 그래서 도메인이 없는 상황에서 부득이하게 openssl을 사용하여 https 통신을 구현하였다.
6.1.1. openssl으로 인증서 발급
(생략, 참고 확인)
6.1.2. nginx.conf ssl 설정
# ...생략
http {
# 가상 호스트 서버 - openssl을 적용한 https 서버
server {
listen 443 ssl;
listen [::]:443;
server_name jaycloud.iptime.org;
server_tokens off;
## ssl 설정 - openssl 전용 ##
ssl_certificate /etc/nginx/ssl/lesstif.com.crt;
ssl_certificate_key /etc/nginx/ssl/lesstif.com.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# ...생략
}
# ...생략
}
6.2. 방법2) nginx certbot을 사용한 ssl 인증서 발급
이 방법을 사용하려면 먼저 정식 도메인이 있어야 한다.
nginx는 certbot을 사용하여 최대 3개월짜리 정식 ssl 인증서를 발급받을수 있다.
6.1.1. nginx certbot 인증서 발급
(생략, 참고 확인)
6.2.1. nginx.conf ssl 설정
# ...생략
http {
# 가상 호스트 서버 - openssl을 적용한 https 서버
server {
listen 443 ssl;
listen [::]:443;
server_name jaycloud.iptime.org;
server_tokens off;
## ssl 설정 - certbot 전용(20년 7월부로 iptme ddns는 사용불가) ##
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ...생략
}
# ...생략
}
참고 자료
1단계 참고
2단계 참고
- 2단계 - 세션과 쿠키https://velog.io/@nmy0502/세션과-쿠키
- 2단계 - # 다른 도메인(프록시 서버o) 간 쿠키 공유https://velog.io/@gbskang/다른-도메인프록시-서버o-간-쿠키-공유
- 2단계, 3단계 - https://cowimming.tistory.com/173
- 2단계 역방향 프록시 설정
3단계 참고
'{사이드 프로젝트} > 22년 프로젝트 BMW' 카테고리의 다른 글
7. API 설계 (0) | 2024.10.09 |
---|---|
3. 인프라1 - On-premise 서버 세팅 (2) | 2023.11.03 |
2. 개발 목표 및 환경 (1) | 2023.11.03 |