세션 기반 인증 = 쿠키
- 쿠키와 세션에서 사용되는 보안 속성(Secure, HttpOnly, SameSite)의 의미와 적용 방법 이해하기
- 각 속성이 방어하는 공격 벡터(XSS, 중간자 공격, CSRF)를 구분하여 설명하기
- 실무에서 사용할 쿠키 발급 규칙, 검증 체크리스트 습득하기
- CSRF 방어 전략(동일 사이트 정책, 동시 토큰, 동기화 토큰)을 워크플로우 + 코드로 구현해보기
- 배포 환경(개발/스테이징/운영)에 따른 설정 프로파일 설계하기
Secure 설정의 이해와 적용
Secure 속성은 암호화된 연결(HTTPS)에서만 쿠키가 전송되도록 제한함
네트워크 구간에서 쿠키가 노출될 위험(중간자 공격, 스니핑)을 크게 줄여줌
1) 개념
- 의미 : 요청이 HTTPS(Spring Security)가 아닐 경우 쿠키가 전송되지 않음
- 효과 : 중간자 공격에 의해 인증 쿠키가 탈취될 위험을 완화함
- 전제 : 서비스 전체가 HTTPS를 사용해야함! 운영 환경에서는 HSTS(Strict-Transport-Security)도 함께 고려함
(1) HSTS(HTTP Strict Transport Security)
클라이언트(브라우저)에게 "이 도메인은 무조건 HTTPS로만 접속해야한다"라는 정책을 전달하는 HTTP 응답 헤더임
예시
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
??
HSTS 속성 설명
max-age=31536000 - > 1년동안 HTTPS만 허용
includeSubDomains - > 모든 서브 도메인에도 적용시킴
preload - > 주요 브라우저 벤더의 HSTS Preload 리스트에 등록함
HSTS 적용 효과
- HTTP 요청이 발생해도 브라우저가 자동으로 HTTPS로 바꿔서 요청함
- 중간자 공격(MITM)이나 다운그레이드 공격을 방어함
2) Secure 적용 예시
HTTP/1.1 200 OK
Set-Cookie: SESSION=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
// 러프한 예시라고 함 : 쿠키 문자열 생성(프레임워크 미사용)
String sessionId = UUID.randomUUID().toString();
String cookie = "SESSION" + sessionId;
+ "; Path=/" // 전체 경로 적용
+ "; Secure" // HTTPS에서만 전송함
+ "; HttpOnly" // JS 접근 차단
+ "; SameSite=Lax" // CSRF 완화
+ "; Max-Age=1800" // 30분후 만료
response.addHeader("Set-Cookie", cookie);
3) 워크플로우 (Secure 포함)
* 운영 환경에서는 HTTPS 강제 리다이렉트와 HSTS를 함께 적용하기
* 개발 환경에서는 로컬 인증서(mkcert 같은것?)를 사용해서 HTTPS를 손쉽게 구성하기
* 서브도메인간 서비스가 섞여있다면, 모든 도메인이 HTTPS를 사용하도록 일괄 적용하기
HttpOnly 설정의 이해와 적용
HttpOnly는 브라우저 자바스크립트에서 쿠키 접근을 차단함
XSS로부터 쿠키 읽기를 어렵게 만들어서 세션 탈취를 완화함
1) 개념
- 의미 : document.cookie로 쿠키값을 읽을 수 있음
- 효과 : 악성 스크립트가 쿠키를 훔쳐가는것을 차단함
- 주의 : XSS 자체를 없애진 않음, 쿠키 탈취를 막아줄 뿐이며 DOM조작은 가능하다
2) 적용예시
Set-Cookie: SESSION=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800
// 쿠키를 HttpOnly로 발급함
String cookie = "SESSION=" + sessionId + "; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800";
response.addHeader("Set-Cookie", cookie);
3) 워크플로우 (HttpOnly 적용)
사용자가 브라우저에 접속 - > 악성페이지가 브라우저에 스크립트 실행을 시도함(콘솔 등으로)
- > HttpOnly가 있으면 JS를 통해 쿠키 읽기 시도가 차단됨
* 인증에 쓰이는 모든 쿠키들은 기본값으로 HttpOnly를 사용함
* SPA에도 인증 토큰을 localStorage에 저장하지 말고, 가능하다면 HttpOnly를 사용하는게 좋음
* CSP(Content-Security-Policy), 출력 인코딩 등 XSS방어도 함께 적용하기(XSS 자체는 차단하지 않음)
SameSite 설정의 이해, 적용
SameSite는 다른 사이트에서 오는 요청에 대해 쿠키가 자동으로 첨부되는지 제어하여 CSRF를 완화함
1) 옵션 비교하기
| 옵션 | 동작 요약 | 대표 용도 | 주의사항 |
| Strict | 크로스 사이트 모든 내비게이션에서 쿠키 미전송 | 민감한 백오피스에 씀 | 소셜 로그인, 외부 리디렉션 시 불편할 수 있음 |
| Lax | 대부분의 크로스 사이트 요청에서 미전송, 탑 레벨 GET 네비게이션은 제외함 |
일반 웹앱 기본값 | 폼 POST, XHR, iframe 요청은 미전송함 |
| None | 출처 상관없이 전송시킴 | 제3자 쿠키 필요(SSO, 임베드) | 반드시 Secure을 동반해야함 |
2) 예시
// 서비스 기본 전략 : Lax
String cookie = "SESSION=" + sessionId + "; Path=/; Secure; HttpOnly; SameSite=Lax";
response.addHeader("Set-Cookie", cookie);
3) 워크플로우(SameSite 적용)
외부사이트에서 브라우저로 폼 전송 - > 브라우저가 우리서버에 요청을 전송함
- > 쿠키가 없다면 인증 실패로 응답해줌
* 기본은 Lax
* 결제 리다렉트, 소셜 로그인 등 크로스 사이트 상호작용이 필수인 경로에 한해서 None; Secure을 제한적으로 사용
* 하위 도메인, 외부 도메인 연동 시 정확한 범위 분석을 문서화해야 헷갈리지 않음
SameSite 정리
목적 : 크로스 사이트 자동 쿠키 전송 제한으로 CSRF 완화
기본값 : Lax 권장
특수 케이스 : SSO, 결제는 None; Secure 검토
CSRF 공격 예방을 위한 설정, 구현
SameSite만으로는 보안이 충분하지 않은 경우가 많은데, 이를 토큰 기반 방어와 함께 적용해야한다
1) CSRF 공격이 무엇인가????????
CSRF(Cross-Site-Request Forgery)
사용자가 자신도 모르게 공격자가 의도한 요청을 특정 웹 애플리케이션에 보내도록 속이는 공격임
원리 : 피해자가 로그인하여 인증 쿠키를 보유중일때,
공격자가 조작된 링크나 폼을 열게 하면 브라우저가 자동으로 쿠키를 첨부하여 서버에 요청을 보냄
가령, 와! 여길 클릭하면 기분이 좋아져요! 하는 링크를 클릭하면 브라우저로 연결되고, 의도한 요청을 보내게 된다...
예시 시나리오:
1. 사용자가 은행 사이트에 로그인하여 세션 쿠키를 요구함
2. 공격자가 이메일에 악성 링크, 가령 <img src="https://wooribank.com/transfer?to=attacker&amount=1000">를 넣음
3. 사용자가 메일을 열면 브라우저가 은행 서버에 요청을 보내고, 자동으로 세션 쿠키가 첨부된다
4. 서버는 정상적인 사용자 요청으로 인식하고 이체를 수행해버림
결과 : 사용자 의도와는 다르게 권한 있는 요청이 실행됨
2) 방어 전략
| 전략 | 개념 | 구현 포인트 |
| 동기화 토큰(Synchronizer Token) | 서버가 세션별 CSRF 토큰 발급, 폼과 함께 제출함 |
서버 저장소에 토큰 보관 후 일치 여부 검증 |
| 더블 서브밋(Double Submit Cookie) | 쿠키로 CSRF 토큰 별도 발급, 본문/헤더값 비교 |
서버는 값 비교만, 저장 불필요(서명 권장) |
| SameSite | 쿠키 자동 전송 제한 | Lax 기본, 특수 경로 None; Secure |
3) 워크플로우(동기화 적용)
브라우저가 서버에 폼 페이지 요청 - > 세션별 CSRF 토큰 생성, 저장 - > 서버가 브라우저에 폼 HTML 전달
- > 브라우저가 서버에게 폼 전송(토큰 포함) - > 서버가 세션저장소에서 토큰이 일치하는지 봄 - > 세션저장소가 서버에 반환
- > 서버가 브라우저에 처리 결과를 응답함
4) 예시(동기화 토큰 러프버전)
// 토큰 발급, 러프버전
byte[] buf = new byte[32];
new java.security.SecureRandom().nextBytes(buf);
String csrf = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
session.setAttribute("CSRF", csrf);
// form인데, form태그랑 input태그임
String form = "<form method=\\"POST\\" action=\\"/transfer\\">"+ // form
"<input type=\\"hidden\\" name=\\"csrf\\" value=\\""+csrf+"\\">"+ // input
"...</form>"; // form
// 검증하기
String presented = request.getParameter("csrf");
String stored = (String)session.getAttribute("CSRF");
if(stored == null || !stored.equals(presented)) {
response.setStatus(403); // CSRF 실패
return;
}
5) 워크플로우(더블 서브밋 버전)
서버가 브라우저에 페이지를 전달하고 CSRF 토큰 쿠키를 발급 - > 브라우저가 서버에게 폼 전송
- > 서버가 쿠키 값과 본문/헤더값을 비교해서 브라우저에게 응답함
6) 예시(더블 서브밋, 러프 버전)
// 발급 : 쿠키와 뷰 모두에 동일한 토큰을 제공함, 쿠키는 HttpOnly 미적용 (비교를 위해 읽어야함)
String csrf = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
String csrfCookie = "CSRF_TOKEN=" + csrf + "; Path=/; Secure; SameSite=Lax";
response.addHeader("Set-Cookie", csrfCookie);
// 전송 : 프론트는 헤더나 숨김 필드로 csrf를 같이 보낸다
String fromCookie = CookieUtil.parse(request.getHeader("Cookie)).get("CSRF_TOKEN");
String fromHeader = request.getHeader("X-CSRF-TOKEN");
boolean ok = fromCookie != null && fromCookie.equals(fromHeader);
if(!ok) {
response.setStatus(403);
return;
}
* 더블 서브밋의 경우, 쿠키 값을 클라이언트가 읽어야하기 때문에 HttpOnly를 적용하지 않는다.
위변조 방지를 위해 서명된 토큰(HMAC)를 사용하면 더 안전함
* 상태 변경 요청(POST, PUT, DELETE)의 경우에만 CSRF 검증을 수행하고, GET은 안전한 동작만 수행함
* 프록시/게이트웨이(Nginx, API Gateway) 레벨에서 SameSite 재설정과 헤더 검증을 보조할 수 있음
* SSO, 결제 리다이렉트 등에서 SameSite=None; Secure가 필요한 경로는 화이트리스트로 관리함
'Spring Boot' 카테고리의 다른 글
| 기본 인증과 인코딩 : 인코딩과 Base64URL (0) | 2025.09.26 |
|---|---|
| 기본 인증과 인코딩 : 기본 인증 (0) | 2025.09.26 |
| 쿠키와 세션 기반 인증 : 세션 기반 인증 (0) | 2025.09.24 |
| 쿠키와 세션 기반 인증 : 쿠키의 개념, 특성 (0) | 2025.09.24 |
| 유저 관리 기능 : 인증 워크플로우 (0) | 2025.09.24 |