-
[Spring] Spring Security, React를 사용하면서 CORS 허용하는 방법Server/Spring 2021. 12. 23. 20:26728x90반응형
Spring에서 CORS 해결하는 법이번 글에서는 개인적인 프로젝트를 하면서 겪었던 SOP 문제를
CORS를 허용해주면서 해결했던 과정에 대해서 공유해보려 합니다. (이 글에서는CORS가 무엇인지에 대해서는 자세히 다루지 않겠습니다.)참고로 프로젝트에서 백엔드는
Spring Boot, 프론트엔드는React를 사용하였습니다.CORS란 무엇일까?CORS가 무엇인지 간단하게 알아보겠습니다.CORS(Cross-Origin Resource Sharing)는교차 출처 리소스 공유라고 합니다. 여기서교차 출처라고 하는 것은다른 출처를 의미하는 것입니다. 즉, 브라우저에서 막고 있기 때문에CORS를 허용해주어야 접근이 가능합니다.출처(Origin)은 무엇일까?
위의 보이는 것처럼 도메인에서
Protocol+Host+Port가 같으면동일한 출처라고 얘기를 합니다. 즉,3개 중에 하나라도 다르면 다른 출처라고 할 수 있습니다.URL 결과 이유 https://gyunny.io/test 같은 출처 Protocol, Host, Port 동일 https://gyunny.io/test?q=work 같은 출처 Protocol, Host, Port 동일 http://gyunny.io/test 다른 출처 Protocol 다름 http://gyunny.com/test 다른 출처 Host 다름 React에서 Spring을 호출한다면?React:http://localhost:3000Spring:http://localhost:8080
React,Spring은 각각 로컬에서 실행하면3000,8080포트로 실행하게 됩니다. 그러면React에서Spring API를 호출하면 어떻게 될까요? 위에서 보았듯이 두 도메인은Port가 다르기 때문에SOP 문제가 발생할 것이라 예측할 수 있습니다.React에서 Spring API 호출해보기
대략적인 그림을 그리면 위와 같이
SOP문제가 발생할 것입니다. 정말SOP문제가 발생하는지 테스트 해보면서 알아보겠습니다.const PostList = () => { useEffect(() => { (async () => { try { const { data } = await axios.get('http://localhost:8080/api/v1/post'); setDataList(data.data); } catch (error) { alert(error); } })(); }, []); }간단하게
React에서Spring API를 호출하는 코드입니다. 이 상태로 브라우저를 실행해서 확인해보겠습니다.

그러면 위와 같이 예상했던 것처럼
SOP문제가 발생하는 것을 볼 수 있습니다.SOP를 해결하기 위해서는Spring Server에서출처가 다른 React 자원이Spring Server 자원에 접근할 수 있도록권한을 주는 작업이 필요합니다.(즉,CORS작업)Spring에서 CORS를 해결하는 방법은 대표적으로 3가지가 있지만 저는 그 중의WebMvcConfigurer인터페이스를 이용해서 해결하는 방법을 적용해보겠습니다.WebMvcConfigurer addCorsMapiings 구현하기@RequiredArgsConstructor @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final LoginUserIdArgumentResolver loginUserIdArgumentResolver; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000") .allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE"); } }WebMvcConfigurer인터페이스가 가지고 있는addCorsMappings메소드를 오버라이딩 한 후에 위와 같이http://localhost:3000에 대해서 접근할 수 있는 권한을 주면 됩니다. 위와 같이 설정만 해주면 너무나도 쉽게SOP를 바로 해결할 수 있습니다.하지만.. 위와 같이 설정을 해주었어도
SOP문제가 해결되지 않았습니다. 이 때왜 SOP문제가 사라지지 않는거지? 서버에서는 분명 접근 권한을 주었는데... 라는 생각이 머리속을 지배하면서 왜 안되는지 원인을 찾기가 쉽지 않았습니다. 결국 많은 삽질과CORS동작 원리를 다시 보면서CORS이슈가 사라지지 않는 원인을 찾았는데요. 원인은 바로 제가Spring Security를 사용하고 있기 때문이었습니다.
시큐리티의 간단한 그림을 보면 위와 같은 구조로 되어 있습니다. 즉,
Interceptor,Controller영역에 들어오기 전에Filter영역을 먼저 거치게 된다는 것을 알 수 있습니다. 즉,Filter에서 걸리기 때문에CORS가 해결되지 않는 것인데요.왜 Filter에서 걸려서 CORS가 해결 되지 않았는지를 이해하려면 Preflight Request에 대해서 알아야 합니다.Preflight Request
Preflight Request는 먼저OPTIONS메소드를 통해 다른 도메인의 리소스로HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인합니다. 즉,Preflight Request요청의 응답이200으로 떨어져야 다음본 요청을 진행할 수 있습니다.그런데
Spring Security Filter에서Preflight Request에 대한 응답을401로 내려주기 때문에CORS문제가 해결되지 않았던 것입니다.
`Response to preflight request doesn't pass access control check: No Access-Contrlol-Allow-Origin header is present is one the requested resource`이번에도 다시 한번 요청을 보내보고 로그를 확인해보면 위와 같이
Preflight Request에서 문제가 생긴 것을 확인할 수 있습니다.@RequiredArgsConstructor @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .mvcMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Preflight Request 허용해주기 .antMatchers("/api/v1/**").hasAnyAuthority(USER.name()); } }Preflight Request가 401로 응답오는 문제를 해결하려면Security Config설정에서 하나 추가해주어야 하는데요. 위의 코드는Security Config파일의 일부분인데, 여기서mvcMatchers를 사용해서Preflight Request OPTIONS 메소드요청을 허용해주면 됩니다.(현재는 위와 같이 해결했지만 추후에는CORS Filter를 적용해볼 생각입니다.)
Security Config에OPTIONS메소드가 오는 경우에 허용을 해주었더니Preflight Request가401이 아닌200으로 응답 오는 것을 확인할 수 있습니다.
그리고
Preflight Request이후에 본 요청도 정상적으로 응답이 오고 잘 작동하는 것까지 확인할 수 있습니다.React Proxy를 사용해서 SOP 해결하기위에서
WebMvcConfigurer인터페이스가 가지고 있는addCorsMappings메소드에서http://localhost:3000에 대한CORS접근 허용 설정을 해주면SOP문제가 해결된다고 하였는데요. 이 부분은Spring Server에서 할 수 있는 것이고React에서도SOP를 해결하기 위해Proxy라는 방법이 존재합니다.바로
Proxy를 알아보기 전에 간단하게 게시글 하나를 불러오는JavaScript코드를 먼저 보겠습니다.export default function PostView() { useEffect(() => { (async () => { try { const { data } = await axios.get( `/api/v1/post/${postId}`, { headers: { 'Authorization': `Bearer ${result}` }, } ); setData(data.data); } catch (error) { alert(error); } })(); }, []); }코드에서
axios로 서버API를 호출하는 곳을 보면절대 주소가 아닌상대 주소로 적혀있는 것을 볼 수 있습니다. 이 상태로서버 API를 호출하면 어떻게 될까요?
상대 주소로 호출해도 자동으로
http://localhost:3000이 앞에 붙는 것을 볼 수 있고 당연하게도http://localhost:3000/api/v1/post/1은 존재하지 않기 때문에404 Not Found가 응답으로 오는 것을 볼 수 있습니다.이러한 문제를 해결하기 위해 필요한 것이 바로
Proxy입니다.React에서 Proxy 적용하기
브라우저에서
React Dev Server를 호출하고React Dev Server에서Proxy를 통해서http://localhost:8080 -> http://localhost:3000로 대체하여Spring Server를 호출하게 됩니다. 즉,Proxy를 통해서{{Base_url}}을 대체할 수 있기에다른 출처로 인식하지 않고같은 출처로 인식하여CORS문제가 발생하지 않는 것입니다.그래서
React에서 Proxy를 설정하는 법에 대해서 알아보겠습니다.yarn add http-proxy-middleware먼저
http-proxy-middleware모듈을 설치하겠습니다.const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware('/api/v1', { target: 'http://localhost:8080', changeOrigin: true, }) ); };그리고
src폴더 아래에setProxy.js라는 파일을 만든 후에 위와 같이 코드를 작성하면Proxy설정이 끝입니다.그런데 문제는 위와 같이 설정해도
Proxy가 적용되지 않는 문제가 있었습니다. 이번에도 왜 안될까.. 하면서 좀 찾아보니 여기 에서 원인을 찾을 수 있었는데요. 추측되는 원인은yarn.lock,node_modules에서Cache하고 있을 수 있어서 지웠다 다시 설치해보라는 내용이었습니다.rm -rf yarn.lock node_modules yarn install그래서 위와 같이 다시 설치한 후에 실행하니 정상적으로
Proxy가 동작하였습니다.
브라우저의
Network탭을 열어보면 이번에도http://localhost:3000으로 요청을 보냐고 있지만404 Not Found가 발생하지 않고 정상적으로 응답이 오는 것을 볼 수 있습니다. 즉,Proxy가 제대로 동작했기에http://localhost:3000으로 요청한 것처럼 보이지만 실제로는http://localhost:8080으로 요청을 보낸 것입니다.글을 마무리 하며당연한 말이지만, 코드나 컴퓨터는 거짓말 하지 않기 때문에.. 문제가 발생하면 분명히 제가 무엇가 잘못해서 그런 것이다를 한번 더 깨닫게 된 것 같습니다. 처음에는
CORS는WebMvcConfigurer인터페이스의addCorsMappings메소드만 오버라이딩 한 후에 설정해주면 쉽게 해결될 것이라는 생각이 저를 우물 안으로 가두었던 것 같습니다.지금까지 매번
Android or iOS와 협업을 해보다 보니CORS를 만날 일이 없었는데 이번 기회에 이론을 넘어서 직접 경험해보면서 해결해보니까 좀 더 뿌듯한 것 같습니다.반응형'Server > Spring' 카테고리의 다른 글
[Spring] Multi-Module에서 Domain 모듈 테스트 실행하는 법 (1) 2022.06.23 [JPA] @OneToOne 관계에서 N + 1 발생하는 이유가 무엇일까? (4) 2022.06.04 [Spring] AWS EC2에서 Spring Access log, logger log 저장하는 법 (0) 2021.12.09 [Spring] 스프링에서 의존성 주입을 하는 3가지 방법 (0) 2021.11.29 [Spring] Transactional Propagation 정리하기 (3) 2021.11.28