CORS 에 대한 설명 은 개발자 면접에서 자주 나오는 질문 중의 하나!
"Cross-Origin Resource Sharing", "교차 출처 리소스 공유", "다른 출처 간의 자원을 공유하는 정책"
다른 출처(도메인, 프로토콜, 포트) 에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하는 웹 보안 메커니즘
한 출처에 있는 자원에서 다른 출처에 있는 자원에 접근하도록 하는 개념
웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때, 교차 출처 HTTP 요청을 실행
browser에서는 보안적인 이유로 cross-origin HTTP 요청들을 제한하기 때문에,
cross-origin 요청을 하기 위해서는 서버의 동의가 필요하다.
cross-origin
cross-origin 은 다음 中 한 가지라도 다른 경우를 말한다.
- 프로토콜 - http와 https는 프로토콜이 다르다.
- 도메인 - domain.com과 other-domain.com은 다르다.
- 포트 번호 - 8080포트와 3000포트는 다르다.
Origin (출처)
- 정의
Protocol + Host + Port 3가지가 같으면, 동일 출처(Origin)라고 한다.
- 예시
- 동일 출처 1 : HTTP 기본 Port인 80번이 생략되어있으므로 동일 출처
- 동일 출처 2
- 다른 출처 1 : Protocol
- 다른 출처 2 : host
- 다른 출처 3 : Port (80, 8080 으로 포트가 다르다)
기존 사이트와 완전히 동일하게 동작하도록 하여, 사용자가 로그인을 하도록 만들고
로그인했던 세션을 탈취하여, 악의적으로 정보 추출 or 타인의 정보를 입력하는 등.. 공격이 가능하다.
→ 다른 사이트에서 원래 사이트를 흉내낼 수 있게 된다.
동일 출처
'요청하는 클라이언트'와 '요청받는 서버'가 같은 출처에 있으면 동일 출처 요청 (동일 출처 정책, Same-Origin Policy)
domain-a.com 유저가 domain-a.com 서버에 요청하면, 동일 정책이기 때문에 아무런 문제 X
(도메인 이외에) 자원
다른 출처
'요청하는 클라이언트'와 '요청받는 서버'가 서로 다른 서버에 있으면 다른 출처 요청 (다른 출처 정책, Cross-Origin Policy)
domain-a.com 유저가 domain-b.com 서버에 요청하면, 호스트(Host)가 다르기 때문에 다른 출처 요청
(도메인 이외에) 자원
https://domain-a.com 의 프론트 엔드 JavaScript 코드가 XMLHttpRequest 를 사용하여 https://domain-b.com/data.json 을 요청하는 경우(즉, 다른 출처)
단순요청(Simple Request), 프리플라이트 요청(Preflighted Request), 인증정보요청(Credential Request)
(실제 요청이 실행되기 이전에 검사를 하고 허용할지말지를 결정할 수 있는 프리플라이트 요청을 권장한다)
브라우저는 다른 출처에 자신의 주소 https://www.site.com 를 origin에 담아서 서버로 요청을 보낸다.
서버는 요청을 확인하고, 다른 출처 주소 https://www.site.com에 에 접근이 가능하다는 access-control-allow-origin 에 해당 주소(https://www.site.com)를 담아서 결과를 리턴
유효한 요청이라면, 리소스를 응답
유효하지 않은 요청이라면, 브라우저에서 이를 막고 에러가 발생
access-control-allow-origin
- CORS 헤더의 중요 요소 중 하나
- 어떤 요청을 허용할지 결정
- 이 헤더 값은 하나의 출처가 될 수도 있고, "*"를 사용해 어떤 출처도 허용하도록 할 수 있다.
예비 요청을 보내지 않고 서버에게 바로 본 요청을 보낸 후,
응답 헤더의 Access-Control-Allow-Origin 값을 확인하여 CORS 정책 위반 여부를 확인
요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 를
제외한 헤더를 사용하면 안 된다.
만약 Content-Type를 사용하는 경우,
application/x-www-form-urlencoded, multipart/form-data, text/plain 만 허용된다.
OPTIONS 를 사용해 자신의 주소 https://www.api.com?q=test 와 origin, access-control-request-method, access-control-request-headers 를 같이 보낸다.
브라우저가 서버에서 응답한 헤더를 보고, 유효한 요청인지 확인
유효한 요청이라면, 정상적인 응답으로 access-control-allow-origin, access-control-allow-method, access-control-allow-headers, access-control-max-age 를 응답받는다.
유효하지 않은 요청이라면, 요청은 중단되고 에러가 발생
요청 헤더 목록
Access-Control-Request-Headers
Preflight Request 시, 실제 요청에서 어떤 header 를 사용할 것인지 서버에게 알리기 위해 사용
Access-Control-Request-Method
Preflight Request 시, 실제 요청에서 어떤 method 를 사용할 것인지 서버에게 알리기 위해 사용
Origin
어디서 요청을 했는지 서버에 알려주는 주소
응답 헤더 목록
Access-Control-Allow-Origin
서버가 허용하는 출처
Access-Control-Allow-Methods
서버가 허용하는 HTTP 메서드 리스트
Access-Control-Allow-Headers
서버가 허용하는 header 리스트
Access-Control-Max-Age
프리 플라이트 요청의 응답을 캐시에 저장하는 시간
일반적으로 사용하는 방식
브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송
서버 응답에 access-control-allow-credentials 가 true로 설정되지 않았거나 access-control-allow-origin 헤더에 있는 값이 허용된 출처가 아니라면 오류가 발생
응답 헤더 목록
Access-Control-Allow-Credentials
- Credentials가 true 일 때, 요청에 대한 응답이 노출될 수 있는지를 나타낸다.
- Preflight Request 에 대한 응답의 일부로 사용될 경우, 실제 자격 증명을 사용하여 실제 요청을 수행할 수 있는지를 나타낸다.
- 간단한 GET 요청은 preflight 되지 않으므로, 자격 증명이 있는 리소스를 요청할 때, 헤더가 리소스와 함께 반환되지 않으면 브라우저에서 응답을 무시하고 웹 콘텐츠로 반환하지 않는다.
CORS 에러가 발생한 상황
spring Boot에서 cross-origin 을 설정하는 방법에는 4가지가 있다.
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin // cross-origin 설정
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
예시 1
@CrossOrigin(origins = "http://example.com", maxAge = 3600) // cross-origin 설정
@RestController
@RequestMapping("/account")
public class AccountController {
// 메서드 1
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
// 메서드 2
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
예시 2
@CrossOrigin(originPatterns = "http://localhost:8080")
@RestController
public class LoginController {
@GetMapping("/login")
public String login() {
return "로그인 성공! ID: jayon, PW: 1234";
}
}
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("http://example.com")
@RequestMapping(method = RequestMethod.GET, "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
Spring 에서 여러가지 CORS 정책을 복합해서 설정 할 수 있다.
retrieve() 메서드
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods(HttpMethod.GET.name())
.maxAge(3000);
}
}
CORS 정책의 설정은 WebMvcConfigurer 를 구현하여 설정할 수 있다.
→ 이는 필터 기반이기 때문에, 전역적으로 모든 요청에 대해서 검사한다.
addCorsMappings() 를 재정의 하면된다.
addMapping() : CORS 정책을 적용할 URL 패턴을 설정
allowedOrigins()
allowedMethods()
maxAge(3000)
filter 를 생성하는 방법이다.
만약, security 를 사용한다면 해당 필터를 security 에 추가해줘야 한다.
CorsConfig 클래스
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**",config);
return new CorsFilter(source);
}
}
SecurityConfig 클래스
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final CorsConfig corsConfig;
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/*/login", "/*/login/**", "/*/signup", "/*/signup/**").permitAll()
.anyRequest().hasRole("USER")
.and()
.addFilter(corsConfig.corsFilter()) // ** CorsFilter 등록 **
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), // Authentication Filter 인증
UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
...
}
Spring Security 사용 시, CORS 설정을 하기 위해서 Authentication Filter 인증보다 앞에 필터를 추가해주어야 한다.
만약 credentialed request가 아닐 경우, config.setAllowCredentials(true) 부분을 제거해야한다
사용 예시
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final CorsConfig corsConfig; private final JwtUtil jwtUtil; private final UserDetailsServiceImpl userDetailsService; private final AuthenticationConfiguration authenticationConfiguration; ... @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf((csrf) -> csrf.disable()); // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정 http.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .requestMatchers("/").permitAll() .requestMatchers("/api/auth/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/homepost").permitAll() .requestMatchers(HttpMethod.GET, "/api/post/{id}/**").permitAll() .anyRequest().authenticated() ); // 필터 관리 http.addFilterBefore(corsConfig.corsFilter(), UsernamePasswordAuthenticationFilter.class); // corsFilter 추가 http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
내가 참가했던 프로젝트에서 사용한 예시를 가져왔다.
해당 방법은 클라이언트에서 프록시 서버를 필요로 할 때 사용하면 좋다.
@Controller
public class UserController {
@GetMapping("/api/view")
public String view() {
return "/cors";
}
@GetMapping("/api/proxy") // localhost:8080/api/proxy 로 요청을 보내면, CORS 이슈 없이 잘 응답이 렌더링된 것을 확인할 수 있다.
@ResponseBody
public String proxyView() {
String url = "http://localhost:1000/login";
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(url, String.class);
}
}
서버 단은 CORS 정책이 위반될 때, 200번 코드를 반환한다.(400, 500번대와 같은 상태 코드가 아닌)
RestTemplate를 통해 서버와 서버 간 통신을 했다.
웹 브라우저에서 리소스 파일들(css, js 등...)은 동일 출처 정책(SOP)에 영향을 받지 않고 로딩이 가능하다.
서버에 <script>
를 호출하여 실행시키는 우회방식 중 하나
단, 리소스 파일을 GET 메서드로 읽어오기 때문에, GET 방식의 API만 요청이 가능!
클라이언트
$.ajax({
url: 'http://localhost:8080/hello?callback=?',
dataType:'jsonp',
jsonpCallback: 'myCallback',
success: function(res) {
console.log(res);
}
});
서버
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}}
}
AbstractJsonpResponseBodyAdvice 클래스
를 사용
@ControllerAdvice 어노테이션과 조합하여 구현
일반적인 헤더 정보 형태
CRLF Injection 한 헤더 정보 형태
헤더의 정보를 우리가 임의로 수정할 수 있다고 할 떄
CR(Carriage Return)과 LF(Line Feed)를 삽입해서, 아래에 있는 헤더 정보를 header body 로 보내 header정보를 무효화 시키는 것이다.
(CRLF : 줄 바꿈)
참고: CORS란 무엇인가?
참고: [Spring] Spring에서 CORS 이슈를 해결하는 방법
참고: [Spring Boot] CORS 설정하기
참고: [이슈해결] CORS with Spring (MVC, Security)
참고: 동일출처정책과 CORS 그리고 해결 방법
참고: 스프링 부트에서 JSONP를 다루는 방법, CORS 이슈 해결하기
참고: SOP, CORS, CSP 개념과 우회방법(Concept & Bypass)