들어가며
처음 프론트와 협업을 해보면서 지금껏 마주보지 못했던 수많은 에러를 마주했다.
그 중 하나가 계속 과제에서 강조했던 CORS 정책 위반이었는데, 처리를 해두었다고 생각했음에도 같은 에러가 났다.
그러므로 오늘의 TIL은 CORS에 대해 알아보고 이를 어떻게 잡을 지 알아보고, 오늘 만난 에러에 대한 트러블 슈팅 또한 해보고자 한다.
CORS(Cross-Origin Resource Sharing)
교차 출처 리소스 공유(CORS, Corss-Origin Resource Sharing)는 일반적으로 개발을 배우기 시작하는 단계쯔음 클라이언트 - 서버 간의 통신을 시도할 때, CORS 정책 위반 에러로 처음 접하게 된다고 한다.
이 CORS는 무엇이고 뭐 때문에 우리에게 에러를 던져줄까?
CORS는 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. 이 CORS는 SOP(Single-Origin Policy)를 허용하기 위해 사용되는 정책인데, CORS를 더 잘 이해하기 위해서 SOP를 먼저 알고 넘어가보자.
SOP(Single-Origin Policy)
동일 출처 정책(same-origin policy)은 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다. 동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.
출처 - MDN
SOP는 쉽게 말하면 ‘같은 출처의 리소스만 공유할 수 있다'를 기본 규칙으로 하는 정책이다.
이는 지난 2011년 RFC 6454 에서 처음 등장한 보안정책이라고 하는데, 이러한 정책이 나타나게 된 이유는 만일 다른 출처의 리소스를 어떠한 검증도 없이 가져다 사용하게 되면 해커의 보안 위협에서 안전하기 어렵기 때문이다.
예를 들어, 해커가 우리가 만든 웹사이트와 완전히 똑같은 형태의 웹사이트를 만들었다고 가정해보자.
우리의 웹사이트에는 쿠키 안에 토큰이 담겨있을 수도 있고 어떤 중요한 정보가 오가고 있을 수 있다. 이때, 해커가 자신의 웹사이트에서 악의적인 정보를 담아 우리의 웹사이트에 요청을 보냈는데 이 SOP 정책이 없다면? 보안 위협을 받아 속절없이 정보를 빼내거나 웹사이트를 망가뜨릴 수도 있는 것이다.
+
해커가 우리 사이트에 보낼 수 있는 보안 위협으로 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting)등이 존재한다. 이 두가지는 추후에 다른 포스팅으로 정리하기로 한다.
이제 우리는 ‘출처'의 의미를 어렴풋이 이해할 수 있다. 하지만 이를 정확하게 무엇인지 설명하라고 하면 쉽지가 않다. 예시와 함께 이해해보자.
아래 표는 URL http://store.company.com/dir/page.html
의 출처를 비교한 예시이다.
예시를 보면 결과가 ‘실패’인 경우의 차이점을 확인할 수 있다.
URL에서 프로토콜, 포트, 호스트가 다를 경우 CORS 정책을 위반했다는 지긋지긋한 에러를 만나볼 수 있을 것이다.
여기서 알 수 있듯이, ‘출처'란 쉽게 말하면 프로토콜, 포트, 호스트같은 기본적인 것들을 합쳐놓은 요청 URL정도로 이해할 수 있다.
SOP를 정리하면, 요청을 보낼 때 기본이 되는 출처와 동일한 출처만 요청을 허용해주는 정책으로 결국 개발자의 보안을 위해 존재한다는 걸 알 수 있다.
이렇게만 보면 참 고마운 정책이다.
하지만 개발을 하면서 다른 출처에 있는 리소스를 가져와서 사용하는 일은 굉장히 흔한 일이다. 당장 서버와 클라이언트 간의 통신을 할 때만 봐도 다른 출처의 리소스를 사용하게 되는데 무작정 이를 막아버린다면 아무것도 할 수 없이 손가락만 빨고 있을 수 밖에 없다.
그러므로 SOP에 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 출처가 다르더라도 허용하기로 했는데, 그 중 하나가 “CORS 정책을 지킨 리소스 요청"이다.
Access to network resources varies depending on whether the resources are in the same origin as the content attempting to access them. Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources retrieved from other origins. For example, an origin is permitted to execute script, render images, and apply style sheets from any origin. Likewise, an origin can display content from another origin, such as an HTML document in an HTML frame.
Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.
출처 - RFC 6454 - 3.4.2 Network Access
하지만 우리가 다른 출처로 요청을 보냈을 때, CORS 정책까지 엄격하게 지켜주지 않는다면 여전히 다른 출처의 요청은 보낼 수 없다.
이 모든것의 대안으로 CORS를 허용해줌으로써 SOP를 우회할 수 있다. 다만 한가지 중요한 사실을 알고있어야하는데, 서로 다른 (혹은 같은) 출처를 비교하는 로직이 서버에 구현된 스펙이 아니라 브라우저에 구현되어 있는 스펙이라는 것이다.
따라서, 서버 개발자가 브라우저를 통하지 않고 서버 끼리만 통신을 할 때는 이 정책이 적용되지 않아 ‘나는 되는데 왜 프론트랑 연결하면 안돼!?!?!’ 의 상황이 발생하는 것이다.
또, CORS 정책을 위반하는 리소스 요청 때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남기기 때문에 CORS가 돌아가는 방식을 정확히 모르면 머리를 싸매는 상황만 반복하게 될 수도 있다. 정확히 어떤 것이 잘못되었는 지 파악하고 싶다면 브라우저의 콘솔을 확인해봐야하는데 매번 프론트 개발자에게 ‘콘솔 에러 로그 좀 보내주세요’ 라고 할 수도 없는 노릇이다.
CORS가 어떻게 동작하는 지는 혹여 잘못된 정보를 전달할 위험을 방지하기 위해 아래의 글들을 참고하도록 하고, 어떻게 CORS 정책을 우회할 수 있는 지 알아보자.
1. CorsFilter 생성하기
첫번째 방식은 커스텀 필터를 생성하는 방식이다.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods","*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization");
if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
}else {
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
}
}
Access-Control-Allow-Origin
에 허용하고자 하는 출처를 작성해주면 해당 출처로 들어오는 요청을 허용해준다. 모든 출처를 허용해준다는 의미의 *
을 사용할 수도 있다.
2. @CrossOrigin 어노테이션 사용
정말 간단한 방법이다. 각 controller의 상단부 혹은 원하는 메소드에 @CrossOrigin(origins = “허용하고자 하는 출처")
를 추가해주면 끝이다. 역시나 *
사용 가능!
하지만 이 방법은 controller가 많아질 수록 설정해야하는 어노테이션도 많아진다는 단점이 있다.
3. WebMvcConfigurer에 설정
WebConfig 파일을 생성해준 후 다음 코드를 추가해준다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("*") // 기타 설정
.allowedHeaders("*");
}
}
여기서 allowedOrigins
는 허용하고자하는 출처를 적어주면 된다. ( *
사용가능)
4. Spring Security를 사용할 때
Spring Security에 CORS 설정을 적용해주는 다양한 방식이 있지만, 나는 다음과 같이 로직을 구현해주었다
... 어노테이션 생략
public class WebSecurityConfig {
... 생략
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
... 나머지 생략
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
프론트의 API 호출 시에 브라우저에서 에러로그를 확인할 수 있어서 아직 이 부분이 해결되었는 지는 확인할 수가 없었다.
추후에 프론트와 연동 후 확인하기로 한다.
+
연동 후, CORS 정책 허용을 중복으로 작성하면 안된다는 점을 깨닫고 Spring Security의 CORS 허용 로직을 삭제함으로써 해결!
트러블 슈팅
문제 - Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 에러
개발 공부를 하면서 정말 기상천외한 에러를 많이 마주하는 것 같다.
이번 에러는 특정버전 이상의 톰캣에서 보안상의 이유로 쿼리스트링에 특정 특수문자가 포함되어 있을 경우 차단을 하기 때문에 발생하는 에러라고 한다
이를 해결하기 위해서는 톰캣을 다운그레이드 하거나, 톰캣 설정파일에 옵션을 추가해줘야한다고 하는데, 굳이 다운그레이드를 할 바에는 설정파일 변경으로 해결할 수 있다고 하니 해당 방법을 이용해보기로 했다.
추가할 옵션 클래스
@Configuration
public class TomcatWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
/**
* 톰캣에 옵션 추가
*
* @param factory
*/
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> connector.setProperty("relaxedQueryChars", "<>[\\]^`{|}"));
}
}
이 역시 서버 간 통신에서는 에러를 마주치지 못해서 프론트와의 연동 후에 테스트 한 후 해결이 되었는 지 확인해보기로 한다.
+
프론트와 연동 후, 테스트 해보니 제대로 작동하는 걸 확인할 수 있었다.
마치며
계속해서 에러가 터지고 해결하지 못한 부분이 나타나는데 API는 계속 수정되어서 내 분에 못이기는 바람에 일찍 게더를 끄고 TIL로 하루를 마무리했다. 조금 이따가 팀원들한테 사과하러 가야지...
출처 및 참고
'Study > TIL' 카테고리의 다른 글
[TIL] 06/22 항해99 45일차 - 오늘 정리한 것 (프록시, 지연로딩, 즉시로딩) (0) | 2022.06.23 |
---|---|
[TIL] 06/21 항해99 44일차 - 오늘 정리한 것 (영속성 관리) (0) | 2022.06.21 |
[TIL] 06/17 항해99 40일차 - Github Readme작성을 위한 마크다운 문법 (0) | 2022.06.18 |
[TIL] 06/16 항해99 39일차 - 여러가지 정리 (0) | 2022.06.16 |
[TIL] 06/15 항해99 38일차 - application.yml 파일 분리하기, AWS S3 적용하기 (0) | 2022.06.16 |