[스프링부트] 42. CORS

백하림's avatar
May 15, 2025
[스프링부트] 42. CORS
💡
CORS : Cross-Origin Resource Sharing
다른 출처(도메인)의 웹 자원(HTML, CSS, JavaScript, 이미지 등)을 내 웹사이트에서 함부로 가져다 쓰지 못하도록 브라우저가 막는 보안 장치
💡

왜 CORS가 필요할까요? (보안!)

만약 CORS가 없다면 악의적인 웹사이트에서 여러분의 민감한 정보가 담긴 다른 웹사이트에 몰래 접근해서 데이터를 빼돌릴 수 있습니다. 마치 허락 없이 옆집 물건을 가져다 쓰는 것과 같은 위험한 상황인 거죠! 그래서 브라우저는 동일 출처 정책(Same-Origin Policy)이라는 기본 보안 규칙을 가지고 있습니다.

"출처(Origin)"가 뭔데요?

출처는 웹사이트의 프로토콜(http/https), 도메인(example.com), 포트 번호(:80, :443 등) 이 세 가지를 합친 것을 말합니다. 이 중 하나라도 다르면 "다른 출처"로 인식됩니다.
예시: http://mywebsite.com:80 <- 같은 출처 https://mywebsite.com:80 <- 다른 출처 (프로토콜 다름) http://mywebsite.com:8080 <- 다른 출처 (포트 번호 다름) http://anotherdomain.com:80 <- 다른 출처 (도메인 다름)

그래서 CORS 에러는 언제 발생하나요?

여러분의 웹사이트(예: http://localhost:3000)에서 다른 도메인의 API 서버(https://api.example.com)에 데이터를 요청하려고 할 때, 브라우저는 이 요청이 교차 출처 요청이라고 판단하고 보안상의 이유로 막아버립니다. 이때 CORS 에러가 발생하는 거죠!

CORS는 어떻게 작동하나요? (서버의 허락!)

CORS 문제를 해결하려면 API 서버 쪽에서 "어떤 출처의 요청까지 허용할 것인지"를 명시해줘야 합니다. 서버는 HTTP 응답 헤더에 Access-Control-Allow-Origin이라는 정보를 담아서 보냅니다.
  • Access-Control-Allow-Origin: *: 모든 출처의 요청을 허용합니다. (개발 환경에서는 편하지만, 실제 서비스에서는 보안상 위험할 수 있습니다.)
  • Access-Control-Allow-Origin: http://mywebsite.com: 특정 출처의 요청만 허용합니다.
브라우저는 서버로부터 응답을 받으면 이 Access-Control-Allow-Origin 헤더를 확인하고, 자신의 출처가 허용된 목록에 있는지 확인합니다. 만약 허용되지 않았다면 응답을 버리고 CORS 에러를 발생시키는 것입니다.

간단 요약!

  • CORS는 다른 출처의 웹 자원 접근을 막는 브라우저 보안 장치입니다.
  • 동일 출처 정책 때문에 기본적으로 교차 출처 요청은 제한됩니다.
  • CORS 에러를 해결하려면 API 서버에서 허용할 출처를 응답 헤더에 명시해야 합니다.

코드

package shop.mtcoding.blog._core.config; import lombok.RequiredArgsConstructor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import shop.mtcoding.blog._core.filter.AuthorizationFilter; import shop.mtcoding.blog._core.filter.CorsFilter; import shop.mtcoding.blog._core.filter.LogFilter; import shop.mtcoding.blog.user.UserRepository; @RequiredArgsConstructor @Configuration public class FilterConfig { private final UserRepository userRepository; @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CorsFilter()); registrationBean.addUrlPatterns("/*"); // 모든 요청에 적용 registrationBean.setOrder(1); // 필터 순서 설정 return registrationBean; } @Bean public FilterRegistrationBean<AuthorizationFilter> authorizationFilter() { FilterRegistrationBean<AuthorizationFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new AuthorizationFilter()); registrationBean.addUrlPatterns("/s/*"); // 모든 요청에 적용 registrationBean.setOrder(2); // 필터 순서 설정 return registrationBean; } @Bean public FilterRegistrationBean<LogFilter> loggingFilter() { FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new LogFilter(userRepository)); registrationBean.addUrlPatterns("/*"); // 모든 요청에 적용 registrationBean.setOrder(3); // 필터 순서 설정 return registrationBean; } }
package shop.mtcoding.blog._core.filter; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String origin = request.getHeader("Origin"); log.debug("origin: {}", origin); response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); // 주소 정확하게 작성 // response.setHeader("Access-Control-Expose-Headers", "Authorization"); // 이 헤더 응답할지 말지 response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Key, Content-Type, Accept, Authorization"); // X가 붙은 건 커스터마이징된 것. response.setHeader("Access-Control-Allow-Credentials", "true"); // 쿠키의 세션값 허용 // Preflight 요청을 허용하고 바로 응답하는 코드 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(200); } else { chain.doFilter(req, res); } } }

OPTIONS

💡
간단하게 말하면, "이 주소로 어떤 종류의 요청을 보낼 수 있나요?"라고 서버에게 물어보는 것과 같습니다.
주요 특징 및 용도:
  • 사전 요청 (Preflight Request): 특히 CORS (Cross-Origin Resource Sharing) 환경에서 중요한 역할을 합니다. 브라우저가 안전하지 않다고 판단되는 교차 출처 (Cross-Origin) 요청 (예: PUT, DELETE, POST with non-standard Content-Type)을 보내기 전에, 서버에게 OPTIONS 요청을 먼저 보내 해당 출처와 메서드를 허용하는지 확인합니다.
  • 서버 기능 확인: 클라이언트는 OPTIONS 요청을 통해 특정 리소스에 대해 서버가 어떤 HTTP 메서드를 지원하는지 (GET, POST, PUT, DELETE, HEAD 등) 확인할 수 있습니다.
  • 추가적인 통신 옵션 확인: 서버는 Allow 헤더 외에도 다른 응답 헤더를 통해 클라이언트에게 추가적인 통신 옵션 (예: Accept-Ranges, Content-Length 등)을 알릴 수 있습니다.
작동 방식:
  1. 클라이언트: 특정 URL에 대해 서버가 지원하는 메서드나 옵션을 알고 싶을 때, HTTP 메서드를 OPTIONS로 설정하여 서버에 요청을 보냅니다.
  1. 서버: OPTIONS 요청을 받으면, 해당 URL에서 지원하는 HTTP 메서드 목록을 Allow라는 HTTP 응답 헤더에 담아 응답합니다. 또한, 필요한 경우 다른 관련 HTTP 헤더를 포함할 수 있습니다.
예시:
클라이언트가 https://api.example.com/data에 대해 서버가 어떤 메서드를 허용하는지 알고 싶다면 다음과 같은 OPTIONS 요청을 보낼 수 있습니다.
OPTIONS /data HTTP/1.1 Host: api.example.com Origin: https://mywebsite.com
서버는 다음과 같은 응답을 보낼 수 있습니다.
HTTP/1.1 204 No Content Allow: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Origin: https://mywebsite.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400
이 응답을 통해 클라이언트는 https://api.example.com/data에 대해 GET, POST, PUT, DELETE, OPTIONS 메서드를 사용할 수 있으며, 특정 CORS 관련 설정 (허용된 Origin, 헤더 등)도 확인할 수 있습니다.
요약:
HTTP OPTIONS 요청은 서버에게 특정 URL에서 허용되는 통신 방법을 문의하는 요청이며, 특히 CORS 사전 요청 메커니즘에서 중요한 역할을 합니다.
notion image
💡
헤더에 담겨서 가는 값들
notion image
💡
OPTIONS 요청 보냄
200 OK 응답 받음
사전 요청(Preflight Request)
notion image

핵심

🔥
CORS: 헤더를 검사하여 Origin이 허용된 것인지 확인하는 것
  1. 프리플라이트 요청 (OPTIONS)
  1. 서버, 프리플라이트 응답 (허용 관련 헤더 포함)
  1. 브라우저, 응답 확인 후 실제 요청 허용/차단 결정
  1. (허용 시) 브라우저, 실제 요청 전송
  1. 서버, 실제 응답
  1. 브라우저, 최종 응답 처리 (CORS 정책 확인)
 
Share article

harimmon