[CORS] 스프링부트 CORS / 리액트 Proxy가 우리를 힘들게 한다... FEAT. 스프링시큐리티 CORS
스프링시큐리티와, webconifg에 두가지 설정법을 다룬다.
토이 프로젝트를 진행 중에 CORS 문제를 만나게 되어 글을 작성하게 되었다.
우리 팀은리액트(3000 포트)와 스프링부트(8080 포트)를 이용해 개발 중에 있다.
토요일 주말, 쉬고 있던 중 한 팀원으로 부터 다음과 같은 연락을 받았다.
"localhost:3000/api/find/team API에서 응답값이 나와요!"
응???
나는 순간적으로 localhost:3000은 리액트 포트인데 무슨 소리지? 하면서 당황했다.
뭔가 착각한거 아니야?라는 마음으로 포스트맨에서
토큰 값과 localhost:3000/api/team/find에 파라미터 값을 넣고 실행해 보니
정말 응답 값이 나왔다.
자 그러면 localhost:3000으로 API 요청을 했는데 정상응답값이 나오는 상황과 이유는 뭔지 알아보자.
문제상황
주어진 상황을 고려해 보면, 프런트엔드에서 백엔드로 요청하는 것이 아닌, 프론트엔드에서 자기 자신으로 요청하는 상황이다.
이렇게 되면 백엔드에서 응답을 받을 수 없으므로, 정상적인 동작이 아니며 예기치 않은 결과가 발생해야 한다.
하지만 프런트엔드에서 프런트엔드로(자기 자신 port로) 포스트맨으로 응답 값 테스트 시 정상 응답 값이 나왔다.
해당 원인은 무엇일까?
해당원인
브라우저에서 실행
API가 실행되는 것이 localhost:3000 (프런트)에서 -> localhost:3000 (프런트)로 요청해도 나오는 것은,
프런트엔드에서 해당 API의 URL을 호출하면, 해당 API URL의 코드가 브라우저에서 실행되기 때문이다.
이 경우는 CORS(Cross-Origin Resource Sharing) 정책이 적용되지 않아
프론트엔드에서 자신의 도메인으로 요청을 보내도 서버에서 처리가 가능하다고 한다.
이를 Same-Origin Policy라고 부르며, 프런트엔드와 백엔드가 같은 도메인이에서 실행되는 경우에만 적용된다.
도메인 / 포트 차이
도메인은 localhost로 같고 포트는 3000, 8080으로 다른 상태이다.
즉 이를 Same-Origin Policy
Same-Origin Policy
Same-Origin Policy는 웹 보안에서 가장 중요한 보안 정책 중 하나로, 웹 브라우저가 다른 출처(origin)로부터 가져온 리소스에 접근하는 것을 제한하는 보안 메커니즘이다.
* 출처
URL 스키마, 호스트 이름,. 포트 번호로 구성된 URL 일부를 의미한다.
만약 브라우저가 현재 열린 웹 페이지의 출처와 다른 출처에서 리소스를 가져온다면
Same-Origin Policy에 따라 해당 리소스에 접근할 수 없다.
이 보안 정책은 다른 출처에서(다른 외부 페이지) 스크립트를 사용하여 사용자 정보를 탈취하거나, CSRF와 같은 공격을 방지하기 위해 필요하다. (CSRF는 다루지 않음) 브라우저는 Same-Origin Policy를 적용하여, 웹 페이지가 리소스를 가져오는 동안 이러한 보안 위협으로부터 사용자를 보호한다.
예를 들어 http://example.co.kr이라는 출처에서 로그인 정보를 입력하고 이 정보를 백엔드 서버로 전송하는데,
이때 로그인 정보를 받아오는 페이지는 같은 출처 http://example.co.kr/login에서만 가져올 수 있다.
만약 다른 출처 http://other.co.kr에서 http://example.co.kr/login에 대한 axios나 ajax를 사용한다면
Same-Origin Policy에 의해 로그인 정보를 가져올 수 없으므로, 이를 이용한 정보 해킹이나, 공격으로부터 방지할 수 있다.
즉 CORS 정책으로 인해 발생하는 문제라고 볼 수 있다.
문제해결
CORS 문제는 프론트엔드와 백엔드 두 곳에서 해결할 수 있는데
여기서는 스프링부트에서 처리하는 방법을 다룬다.
스프링부트 처리 방법
스프링부트에서 처리하는 방법으로는
Controller마다 CORS 설정을 주어 처리하는 방법과
WebConfig를 이용해서 전역적으로 해결하는 방법이 있다.
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://example.com",
"https://www.example.com",
"http://localhost:3000/",
"https://example.app/")
.allowedMethods("*");
}
1. Config 폴더에 WebConfig파일을 만들어 주고, WebMvcConfigurer 인터페이스를 선언
2. .addMapping은 어떤 경로의 요청인지 정의
3. .allowedOrigins는 어떤 도메인으로 부터의 요청인지 정의
- 보통 프론트엔드에서 로컬, 개발 서버에서 요청을 하므로, http://localhost:3000과 개발서버를 선언한다.
4. .allowedMethod는 DELETE, POST, GET, PUT 메소드 중 어떤 메소드를 허용할지 정의를 한다
위의 방법으로 해줬는데 CORS 문제가 지속적을 발생했다.
따라서 아래와 같이 allowCredentials(true)를 추가해줬더니 해결됐다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
Controller마다 선언하는 방법은 내가 생각하기에 비효율적이라고 생각하여 WebConfig만 다뤘다.
스프링시큐리티 CORS 적용
추가적으로 스프링 시큐리티를 이용하고 있다면
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/api/auth/**").permitAll()
.antMatchers(PERMIT_URL_ARRAY).permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(new JwtAuthFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
다음과 같이 cors()를 추가해줘서 시큐리티의 필터 앞단에서 CROS 적용을 허용 해줘야한다.
http.cors().and()
나의 경우는 스프링시큐리티를 적용해서 CORS 문제를 해결할 수 있었다.