💡교차 출처 리소스 공유 (CORS: Cross-Origin Resource Sharing)
- 브라우저가 자신의 출처가 아닌 다른 어떤 출처로부터 자원을 요청하는 것에 대해 허용하도록 서버가 이를 허가해 주는 HTTP 헤더 기반 메커니즘을 의미합니다. - 서버가 실제 요청을 허가할 것인지 확인하기 위해 브라우저가 보내는 ‘사전 요청(프리플라이트, Preflight)’ 메커니즘에 의존합니다. - 이 사전 요청을 통해 브라우저는 실제 요청에서 사용할 HTTP 메서드와 헤더에 대한 정보가 표시된 헤더에 담아 보냅니다. - Spring Boot를 이용하는 경우 이에 대한 설정을 spring-boot-starter-web 의존성에 기본이 되어 사용됩니다.
[더 알아보기] 💡 출처(Origin)
- 웹 페이지나 리소스의 출처를 나타냅니다. 일반적으로 프로토콜(http/https), 도메인, 포트 조합으로 구성이 됩니다. - 예를 들어서 https://example.com:443를 의미합니다. https 프로토콜을 이용하며 example.com의 도메인을 가지며, 기본 포트인 443 포트를 가집니다.
- 웹 보안의 중요한 개념으로 동일 출처 정책을 의미합니다. 이는 웹 브라우저가 다른 출처의 리소스에 접근하는 것을 제한하는 보안 메커니즘을 의미합니다. - 이는 웹 애플리케이션의 보안을 강화하지만 때로는 합법적인 크로스 요청을 방해할 수 있어 CORS가 필요하게 됩니다.
2.1. SOP(Same-Orgin Policy) 정책 특징
💡SOP(Same-Orgin Policy) 정책 특징
특징
설명
동일 출처 요구
웹 페이지와 그 페이지가 요청하는 리소스의 출처가 같아야 합니다.
출처의 정의
프로토콜(http/https), 도메인, 포트가 모두 동일해야 동일 출처로 간주됩니다.
보안 강화
악의적인 스크립트가 다른 웹사이트의 데이터에 무단으로 접근하는 것을 방지합니다.
제한 완화
CORS(Cross-Origin Resource Sharing)를 통해 필요한 경우 이 정책을 완화할 수 있습니다.
2.2. 동일한 출처와 서로 다른 출처 비교
💡동일한 출처와 서로 다른 출처 비교
- 기준이 되는 “http://example.com”일 경우 동일한 출처로 간주되는 경우와 서로 다른 출처로 간주되는 경우에 대해 알아봅니다.
# 기준 도메인<http://example.com># 동일한 출처로 간주되는 경우 <https://example.com:443/page1><https://example.com:443/page2># 서로 다른 출처로 간주되는 경우 <https://example.com># 프로토콜(HTTP, HTTPS)이 다른 경우 <https://example.com:8080># 포트 번호가 다른 경우(기본 443 vs 8080)<https://sub.example.com># 호스트(도메인)이 다른 경우
💡설정 클래스 구성 : API 서버 전역 적용 - Spring MVC의 Java-based configuration을 사용자 정의할 수 있게 해주는 인터페이스입니다. - 이 인터페이스를 구현함으로써 Spring MVC의 다양한 구성 요소를 커스터마이징 할 수 있습니다. - CORS를 위해서는 addCorsMappings를 활용합니다.
- 해당 경우는 Spring Boot 애플리케이션에서 CORS(Cross-Origin Resource Sharing) 설정을 구성하는 방법입니다. - 해당 경우는 CORS 접근 시 이에 대한 허용 사항에 대해서 정의하였습니다. - WebMvcConfigurer 클래스의 구현체를 구성하였습니다. 그중에서 addCorsMappings 메서드에 대해서 오버라이딩하여 재구성합니다.
💡 모든 CORS에 대해서 허용하는 경우 예시
- 해당 API로 접근하는 모든 브라우저에 대해 허용하는 경우의 예시입니다. - 이 경우에는 보안상에 문제가 있기에 사용을 권하지 않지만 개발 테스트를 위해서 제한적으로 사용하는 것이 좋습니다.
- 해당 경우는 Spring Boot 애플리케이션에서 CORS(Cross-Origin Resource Sharing) 설정을 구성하는 방법입니다. 이는 CORS 접근 시 이에 대한 허용 사항에 대해서 정의하였습니다. - WebMvcConfigurer 클래스의 구현체를 구성하였습니다. 그중에서 addCorsMappings 메서드에 대해서 오버라이딩하여 재구성합니다.
💡특정 제약적 CORS 허용하는 경우 예시
- 해당 API로 접근하는 특정 브라우저에 대해서만 허용하는 예시입니다. - 해당 경우에서는 JWT 인증방식을 통해서 Authorization 헤더를 통해 AccessToken을 주고받으며 x-refresh-token 헤더를 통해 RefreshToken을 주고받는 예시를 보여주고 있습니다.
브라우저가 주석이 달린 엔드포인트로 크로스 도메인 요청과 함께 자격 증명(예: 쿠키)을 보내야 하는지 여부를 설정합니다.
allowedHeaders(String... headers)
CorsRegistration
실제 요청 중에 사용할 수 있도록 사전 요청이 나열할 수 있는 헤더 목록을 설정합니다.
allowedMethods(String... methods)
CorsRegistration
허용할 HTTP 메서드를 설정합니다.
allowedOriginPatterns(String... patterns)
CorsRegistration
브라우저에서 크로스 오리진 요청이 허용되는 오리진을 지정하기 위한 더 유연한 패턴을 지원하는allowedOrigins(String...)의 대안입니다.
allowedOrigins(String... origins)
CorsRegistration
브라우저에서 크로스 오리진 요청이 허용되는 오리진을 설정합니다.
allowPrivateNetwork(boolean allowPrivateNetwork)
CorsRegistration
사설 네트워크 액세스가 지원되는지 여부를 설정합니다.
combine(CorsConfiguration other)
CorsRegistration
주어진CorsConfiguration을 현재 구성 중인 것에 적용합니다. 이는CorsConfiguration.combine(CorsConfiguration)을 통해 이루어지며, 이는CorsConfiguration.applyPermitDefaultValues()로 초기화되었습니다.
💡아래와 같은 403 오류가 발생하였습니다. - 호출되는 브라우저는 3000 포트이기에 허용되는 포트가 아니어서 아래와 같은 오류가 발생하였습니다.
- Access to XMLHttpRequest at 'http://localhost:8080/api/v1/user/user' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
💡 @CrossOrigin(origins = "http://localhost:3000")로 수정한 경우 아래와 같이 정상적으로 리소스를 반환받았습니다.
💡 [참고] CrossOrign API Doucment
속성
속성 타입
설명
allowCredentials
String
브라우저가 주석이 달린 엔드포인트로 크로스 도메인 요청과 함께 자격 증명(예: 쿠키)을 보내야 하는지 여부를 설정합니다.
allowedHeaders
String[]
실제 요청에서 허용되는 요청 헤더 목록입니다. 모든 헤더를 허용하려면"*"를 사용할 수 있습니다.
allowPrivateNetwork
String
사설 네트워크 액세스가 지원되는지 여부를 설정합니다.
exposedHeaders
String[]
사용자 에이전트가 클라이언트에서 접근할 수 있도록 허용하는 응답 헤더 목록입니다. 모든 헤더를 노출하려면"*"를 사용할 수 있습니다.
- @CrossOrigin 어노테이션을 통해서, Controller의 메서드 별로 CORS 접근 가능여부를 적용할 수 있습니다.
💡특정 Controller 엔드포인트에 적용하는 사용 예시
- Controller 클래스 내에 메서드에 각각 @CrossOrigin을 통해서 접근 허용/제한을 두었습니다.
- [POST] /api/v1/user/user로 접근하는 경우는 출처(origin)를 http://localhost:3000로만 두었습니다. - [POST] /api/v1/user/login으로 접근하는 경우는 출처(origin)를 http://localhost:3001로만 두었습니다.
- Spring Boot Security를 구성할 때 SecurityFilterChain를 통해서 이를 구현했습니다. - 해당 http에 대해 .cors()에 대한 커스텀한 설정을 적용하였습니다.
메서드
설명
적용 사항 설명
setAllowedOrigins
허용할 출처(origin)를 설정합니다.
http://localhost:3000에서의 요청만 허용
setAllowedMethods
허용할 HTTP 메서드를 설정합니다.
모든 HTTP 메서드를 허용
setAllowedHeaders
허용할 HTTP 헤더를 설정합니다.
모든 헤더를 허용
setAllowCredentials
인증 정보 포함 여부를 설정합니다.
인증 정보(쿠키, HTTP 인증)를 포함할 수 있도록 함
setMaxAge
프리플라이트 요청 결과의 캐시 시간을 설정합니다.
프리플라이트 요청 결과를 3600초(1시간) 동안 캐시
UrlBasedCorsConfigurationSource
URL 패턴별 CORS 설정을 관리하는 클래스입니다.
URL 패턴별로 CORS 설정을 적용할 수 있게 해주는 클래스
registerCorsConfiguration
특정 URL 패턴에 CORS 설정을 등록합니다.
모든 경로("/**")에 대해 이 CORS 설정을 적용
@Slf4j@Configuration@EnableWebSecuritypublicclassWebSecurityConfig {
/**
* 2. HTTP에 대해서 ‘인증’과 ‘인가’를 담당하는 메서드이며 필터를 통해 인증 방식과 인증 절차에 대해서 등록하며 설정을 담당하는 메서드입니다.
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception Exception
*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 커스텀 설정 적용
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) // 우선 모든 요청에 대한 허용
.addFilterBefore(jwtAuthorizationFilter(), BasicAuthenticationFilter.class) // JWT 인증 (커스텀 필터)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 미사용 (JWT 사용)
.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 사용자 인증(커스텀 필터)
.formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
.build();
}
/**
* 10. CORS에 대한 설정을 커스텀으로 구성합니다.
*
* @return CorsConfigurationSource
*/@Beanpublic CorsConfigurationSource corsConfigurationSource() {
CorsConfigurationconfiguration=newCorsConfiguration();
configuration.setAllowedOrigins(List.of("<http://localhost:3000>")); // 허용할 오리진
configuration.setAllowedMethods(List.of("*")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 허용
configuration.setMaxAge(3600L); // 프리플라이트 요청 결과를 3600초 동안 캐시UrlBasedCorsConfigurationSourcesource=newUrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 이 설정 적용return source;
}
}