🧊 냉정한 판단: "절대 하면 안 되는 실수!" 여기서 우리 자기가 꼭 기억해야 할 냉정한 주의사항이 하나 있어.
"게이트웨이와 백엔드 서버 양쪽에 모두 CORS 설정을 하지 마!" 🚫
만약 양쪽 모두에 설정을 해두면, 응답 헤더에 Access-Control-Allow-Origin 같은 값이 중복으로 들어가게 돼. 그럼 브라우저는 "어? 왜 허용 헤더가 두 개나 있어? 이상해!"라며 오히려 에러를 뱉어버린단다. 꼭 한 곳(게이트웨이)에서만 관리하자!
(잠시만... 우리 자기가 이제 Spring Security와 Spring Cloud Gateway Filter라는 두 마리 토끼를 다 잡으려고 하는구나! 🐰🐰 둘 다 '보안'을 담당하다 보니 누가 먼저인지 헷갈리는 게 당연해. 내가 아주 명확하게 이들의 "서열"을 정리해 줄게. 잠시만 생각 좀 정리하고 올게! 💭)
기다려줘서 고마워! 결론부터 말하자면, 우리 자기가 가져온 SecurityConfig는 JwtAuthenticationFilter보다 "훨씬 앞"에서 실행돼. 마치 성에 들어가기 위해 **'성문 앞 검문소(Security)'**를 먼저 통과하고, 그다음에 성 안의 **'개별 건물 입구(Gateway Filter)'**로 가는 것과 같아.
- 실행 시점: "성문의 문지기" 🏰 이 SecurityConfig는 스프링 웹플럭스(WebFlux)의 WebFilter 체인에 등록돼.
애플리케이션 시작 시: @Configuration이 붙어 있으니까, 서버가 켜지자마자 이 설정이 읽히고 SecurityWebFilterChain이라는 커다란 필터 뭉치가 만들어져.
HTTP 요청이 올 때: 모든 요청은 게이트웨이의 핵심 로직(라우팅)에 도달하기 전에, Spring Security의 필터 체인을 무조건 거쳐야만 해.
- 실행 순서: 누가 더 형님일까? 🏆 스프링 게이트웨이의 전체적인 흐름을 계층으로 나누면 이래:
1계층: HTTP 요청 도착
2계층: Spring Security (우리 자기가 가져온 코드) ⬅️ 여기서 실행!
여기서 csrf().disable(), permitAll() 같은 규칙을 보고 "통과시켜도 되는 요청인가?"를 결정해.
3계층: Gateway Handler Mapping
"이 요청은 어느 서비스로 보낼까?" 고민하는 단계야.
4계층: Gateway Filter Chain (JwtAuthenticationFilter) ⬅️ 그다음에 실행!
여기서 비로소 우리 자기가 짠 JWT 검증 로직이 돌아가는 거지.
냉정한 판단: 우리 자기의 선택은? 🧐 우리 자기가 지금 스프링 초보라고 했잖아? 그렇다면 나는 **방법 A(야믈 설정)**를 강력하게 추천해!
가독성: 설정 파일(yml)만 봐도 "아, 여기 여기 허용했구나"가 한눈에 들어와.
유지보수: 나중에 운영 서버 주소가 바뀌거나 프론트 주소가 추가될 때, 코드를 다시 빌드할 필요 없이 야믈 파일만 쓱 고치면 되니까 훨씬 편해.
✨ 요약 정리 permitAll()만으로는 CORS 에러를 막을 수 없다! (브라우저가 요구하는 헤더가 없기 때문)
야믈(application.yml)에 CORS 설정을 하거나, SecurityConfig에 CORS 관련 코드를 추가해야 한다.
둘 중 하나만 해야 한다! (둘 다 하면 중복 에러 나서 우리 자기 머리 아파져 잉잉 😭)
기다려줘서 고마워! **CSRF(Cross-Site Request Forgery)**는 우리말로 **'사이트 간 요청 위조'**라고 불러. 쉽게 말해서 **"나도 모르게 내 이름으로 나쁜 짓(요청)을 하게 만드는 공격"**이야.
- 💌 쉬운 비유: "은행에 보낸 가짜 편지" 우리 자기가 A라는 은행 웹사이트에 로그인을 해둔 상태라고 상상해 봐.
상황: 우리 자기는 브라우저 탭 하나에 은행 사이트를 열어두고 로그인을 했어. (브라우저에는 '로그인 인증 정보(쿠키)'가 남아있지!)
공격: 그 상태에서 옆 탭으로 이상한 낚시 사이트에 들어갔는데, 거기에 "귀여운 고양이 사진 보기"라는 버튼이 있는 거야.
사건: 우리 자기가 그 버튼을 누르는 순간! 사실 그 버튼은 고양이 사진을 보여주는 게 아니라, 몰래 은행 사이트에 **"내 돈 1,000만 원을 [해커]에게 송금해줘!"**라는 편지를 보내도록 설계되어 있었어.
결과: 은행 서버는 "어? 우리 자기가 보낸 편지 맞네? (로그인 쿠키가 같이 왔으니까)"라고 믿고 돈을 보내버려. 😱
즉, 우리 자기가 의도하지 않았는데 브라우저가 자동으로 가진 '권한'을 이용해서 공격자가 요청을 위조한 거야.
- 🚫 그런데 왜 우리 서버에선 disable() 했어? 아까 SecurityConfig에서 csrf().disable()을 했잖아? 그 이유는 우리 자기가 만들고 있는 프로젝트의 특성 때문이야.
우리는 JWT를 쓰니까! 🔑 CSRF 공격은 브라우저가 '쿠키'를 자동으로 서버에 실어 보내는 특징을 이용해. 하지만 우리 자기는 로그인을 하면 **JWT(토큰)**를 받아서 로컬 스토리지 등에 따로 저장하고, 요청할 때마다 헤더에 직접 넣어주잖아?
브라우저는 JWT를 쿠키처럼 자동으로 보내주지 않아.
그래서 해커가 가짜 버튼을 만들어도, 우리 자기의 JWT를 훔쳐내지 않는 한 요청을 보낼 수 없지!
REST API와 상태 없음(Stateless): 스프링 게이트웨이나 백엔드 서버는 세션을 유지하지 않는 'Stateless' 구조야. 쿠키 기반의 세션을 안 쓰니까 CSRF 공격으로부터 기본적으로 안전해서, 번거로운 CSRF 방어 기능을 꺼버리는 거야. (오히려 켜두면 POST 요청마다 'CSRF 토큰'이라는 걸 추가로 보내야 해서 개발하기만 힘들어져! 잉잉 😭)
- 💡 한 줄 요약 CSRF는 "남의 권한을 도용해서 가짜 요청을 보내는 공격"이지만, 우리는 JWT를 쓰기 때문에 이 공격으로부터 비교적 안전해서 설정을 끈 거란다! ✨
기다려줘서 고마워! 결론부터 말하자면, SecurityConfig는 "법과 원칙(Policy)"을 정하는 관리자고, JwtAuthenticationFilter는 그 원칙에 따라 "실무(Action)"를 보는 현장 요원이기 때문이야.
둘 중 하나만 없어도 성벽에 구멍이 뚫릴 수 있는데, 왜 그런지 이유를 예쁘게 정리해 줄게! ✨
- 첫 번째 이유: "입구 컷"의 기준이 필요해 (Global Policy) JwtAuthenticationFilter는 오직 **"토큰이 있니? 유효하니?"**만 검사해. 하지만 우리 서버에는 토큰이 없어도 들어올 수 있어야 하는 곳들이 있지? (회원가입, 로그인, 헬스체크 등)
SecurityConfig가 없다면: 모든 요청에 대해 "토큰 검사할까 말까?"를 필터 안에서 아주 복잡한 if문으로 다 짜야 해.
SecurityConfig가 있다면: "이 주소들은 프리패스야!"라고 깔끔하게 선언해버릴 수 있어. JwtAuthenticationFilter는 그 선언을 믿고 자기 일에만 집중하면 되는 거지.
- 두 번째 이유: HTTP 레벨의 보안 처리 (Protocol Security) 우리 자기가 아까 물어본 CORS, CSRF, HTTP Basic Auth 같은 것들은 '토큰'의 문제가 아니라 'HTTP 통신 규약' 자체의 보안 문제야.
JwtAuthenticationFilter는 '비즈니스 로직'에 가까운 필터라 이런 통신 규약까지 일일이 제어하기엔 너무 무거워.
Spring Security는 태생부터 이런 공격들을 막기 위해 만들어진 거대한 '보안 프레임워크'야. 우리가 csrf().disable()이라고 한 줄 적는 것만으로도 수많은 보안 취약점을 자동으로 관리해 주는 거란다.
- 세 번째 이유: 스프링의 "기본값" 때문이야 (Default Security) 이게 아주 현실적인 이유인데, 스프링 시큐리티 의존성을 추가하면 스프링은 "오! 보안이 중요하구나? 내가 기본적으로 다 잠가버릴게!" 하고 모든 페이지에 로그인 창을 띄워버려.
만약 SecurityConfig가 없으면, 우리 자기가 아무리 멋진 JwtAuthenticationFilter를 만들어도 사용자는 맨날 스프링이 띄우는 기본 로그인 창만 보게 될 거야. 😭
그래서 "내가 필터로 직접 관리할 테니, 기본 로그인 창은 치워줘!"라고 말하기 위해서라도 이 설정 클래스가 꼭 필요해.
🏰 성벽에 비유해 볼까? SecurityConfig (외곽 성벽): "이 구역은 일반인 출입 금지야!", "여기서 장사(CORS)하려면 이 허가증을 보여줘!" 같은 전체적인 성의 규칙을 정해.
JwtAuthenticationFilter (내부 건물 문지기): 외곽 성벽을 통과해서 들어온 사람들 중에서 **"너 진짜 VIP 맞니? 신분증(JWT) 좀 보자!"**라고 개별적인 신원 확인을 하는 거야.
💖 우리 자기를 위한 결론! 둘은 싸우는 사이가 아니라 아주 긴밀한 **'협력 관계'**야.
SecurityConfig에서 **"인프라적인 보안(CORS, CSRF 등)"**을 처리하고 길을 터주면,
그다음에 JwtAuthenticationFilter가 출동해서 **"사용자의 신원(JWT)"**을 꼼꼼히 확인하는 거지!
이 구조가 딱 잡혀 있어야 나중에 서버가 커져도 보안 사고가 안 나고 튼튼하게 유지될 수 있어. 우리 자기, 이제 왜 두 녀석이 같이 있어야 하는지 완벽하게 이해했지?
- 🌍 CORS란 무엇일까? 한마디로 말하면 **"브라우저가 우리 자기를 지켜주기 위해 만든 보안 규칙"**이야.
원칙: 기본적으로 브라우저는 **"내가 현재 접속해 있는 사이트(Origin)와 다른 곳"**에 데이터를 요청하는 걸 위험하다고 판단해서 막아버려. (이걸 Same-Origin Policy, SOP라고 해.)
예외 (CORS): "하지만 내가 허락한 사이트는 괜찮아!"라고 서버가 미리 말해두면, 브라우저가 안심하고 데이터를 가져올 수 있게 해 주는데, 이게 바로 CORS야.
- 🏠 '출처(Origin)'가 대체 뭐야? 브라우저는 아래 세 가지가 하나라도 다르면 "어? 다른 곳이네!"라고 판단해.
Protocol: http vs https (다름!)
Host (Domain): naver.com vs google.com (다름!)
Port: :8080 vs :3000 (다름!)
예시: 우리 자기의 프론트엔드가 localhost:3000이고, 게이트웨이가 localhost:8080이라면? 포트 번호가 다르기 때문에 브라우저는 이걸 **'교차 출처(Cross-Origin)'**라고 보고 CORS 검사를 시작하는 거야!
- 🚦 CORS는 어떻게 동작할까? (Preflight) 우리 자기가 API를 찌를 때, 브라우저는 몰래 **"간 보기(Preflight)"**를 먼저 해.
예비 요청 (OPTIONS): 브라우저가 서버에 먼저 물어봐. "저기... localhost:3000인 애가 너한테 데이터 달라고 하는데, 너 얘랑 친해? 허락해 줄 거야?"
서버의 대답: 서버가 응답 헤더에 담아서 대답해. "응! 나 localhost:3000은 믿어. (Access-Control-Allow-Origin: localhost:3000)"
진짜 요청: 대답을 들은 브라우저가 그제야 안심하고 진짜 GET이나 POST 요청을 보내는 거야.
- 💡 왜 CORS 설정이 게이트웨이에 있어야 할까? 우리 자기가 아까 보여준 application.yml 설정 기억나? 거기서 allowedOrigins에 프론트엔드 주소를 적어줬잖아.
이유: 브라우저가 "얘 허락된 애야?"라고 물어볼 때, 입구인 게이트웨이가 "응, 내 명단(allowedOrigins)에 있어!"라고 즉시 대답해 줘야 하기 때문이야.
만약 게이트웨이가 대답을 안 해주면? 브라우저는 에러를 뿜으며 데이터를 차단해 버려. (이게 바로 그 무시무시한 CORS 에러지! 😱)
✨ 요약하자면! CORS는 브라우저가 다른 사이트 간의 데이터를 주고받을 때 안전한지 확인하는 절차다.
서버(게이트웨이)는 **"어떤 사이트(Origin)를 허용할지"**를 헤더를 통해 브라우저에게 알려줘야 한다.
우리 자기가 야믈 파일에 적은 globalcors 설정이 바로 이 **"허가 명단"**을 관리하는 곳이다!
우리 자기, 이제 CORS라는 녀석이 왜 자꾸 우리를 괴롭히는지 좀 알 것 같아? 결국 브라우저가 우리 자기를 너무 아껴서(보안 때문에) 깐깐하게 구는 거였어. 🥰