1. CORS를 이해하기 위한 기본 개념 - Origin
Origin은 URL에서 프로토콜, 도메인, 포트 번호를 합친 부분을 의미한다.
예를 들어, 다음과 같은 URL이 있다고 해보자.
URL : https://it-eldorado.com:80/posts/123456?data=789#abc
여기서 프로토콜(Scheme이라고도 함)에 해당하는 부분은 https://이고, 도메인에 해당하는 부분은 it-eldorado.com이며, 포트 번호에 해당하는 부분은 :80이다. 따라서 Origin은 https://it-eldorado.com:80이다.
2. CORS를 이해하기 위한 기본 개념 - SOP (Same Origin Policy)
SOP는 다른 Origin으로 요청을 보낼 수 없도록 금지하는 브라우저의 기본적인 보안 정책이다.
만약 SOP가 없다면 공격자는 사이트에 악성 스크립트를 심어 놓을 수 있다. 예를 들면, 다음과 같은 XSS 공격이 가능해진다.
<script>
fetch('http://google.com').then(/**/) // 이후 공격자에게 전송
</script>
이 공격을 통해 공격자는 피해자가 'google.com'에 접속한 화면을 볼 수 있을 뿐만 아니라,
(가능하다면) 쿠키 값을 가져와 세션을 탈취할 수도 있을 것이다.
3. CORS란?
CORS(Cross Origin Resource Sharing)는 다른 Origin으로 요청을 보내기 위해 지켜야 하는 정책으로, 원래대로라면 SOP에 의해 막히게 될 요청을 풀어주는 정책이다.
동작 과정은 다음과 같다.
1. 기본적으로 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더의 Origin 필드에 요청을 보내는 출처를 함께 담아보낸다.
2. 이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려준다.
3. 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.
만약 유효하지 않다면 그 응답을 사용하지 않고 버린다. 이러한 상황이 CORS 에러다.
위의 경우에는 유효하므로, 리소스를 문제없이 가져오게 된다.
CORS의 시나리오로는 1) Preflight / 2) Simple / 3) Credentialed Request 가 있다.
- 1) Preflight request
브라우저가 본 요청을 보내기 전에 Preflight라고 부르는 예비 요청을 보내고, 이 Request에는 OPTION 메소드가 사용된다.
서버는 이 예비 요청에 대한 Response로 허용하는/금지하는 정보를 응답 헤더에 담아 보내준다.
친구의 집에 놀러가야하는 상황에서, 놀러가기 전에 놀러가도 되는지 확인하지 않는다면 큰 민폐일 수 있다. 때문에 친구에게 물어보고 가야할텐데 이 같은 작업이 Preflight Request라고 생각하면 쉽다.
이후 브라우저는 Request와 Response의 허용 정책을 비교한 후에, 안전하다고 판단되면 같은 end-point로 다시 본 요청을 보내게 된다.
이 과정을 통해 브라우저 스스로 이 요청을 보내는 것이 안전한지를 확인한다.
- 2) Simple Request
이는 예비 요청을 생략하는 방법으로 간단하지만, 충족해야하는 조건들이 있다.
1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
1번 같은 경우에는, 에러가 포함된 요청이 서버까지 가게 되면 PUT이나 DELETE 등 DB 상에 상이한 로직이 발생할 수 있기 때문이다.
그 이외에 조건은 만족시키기에 까다로워, 경험하기 쉽지는 않다.
4. CORS 에러 해결 방법
Controller에서 @CrossOrigin 어노테이션 추가하기
컨트롤러에서 특정 메서드 혹은 컨트롤러 상단부에 @CrossOrigin 만 추가하면 된다. 그러나 해당 방법의 경우 CORS 정책을 설정해야 할 대상이 많아지면 중복된 코드가 늘어나는 단점이 존재한다.
@RestController
@RequestMapping(value = "/api/threats", produces = "application/json")
@CrossOrigin(origins = "http://front-server.com") // 컨트롤러에서 설정
public class ThreatController {
private final ThreatService threatService;
public ThreatController(ThreatService threatService) {
this.threatService = threatService;
}
@GetMapping
@CrossOrigin(origins = "http://front-server.com") // 메서드에서 설정
public ResponseEntity<ThreatLogCountResponse> getAllThreatLogs() {
return ResponseEntity.ok(threatService.getAllThreatLogCount());
}
}
WebMvcConfigurer를 이용해서 처리하기
앞서 말한 단점 때문에, 글로벌하게 config 파일에서 CORS 정책을 설정하는 것이 좋다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods(HttpMethod.GET.name());
}
}
WebMvcConfigurer를 상속한 클래스를 만들고, addCorsMappings() 를 재정의하면된다. addMapping() 메소드를 통해 CORS 정책을 적용할 URL 패턴을 설정하고, allowedOrigins() 메소드를 통해 허용할 출처를 적어준다. 이 외에 allowedMethods() 메소드를 통해 GET, POST와 같은 HTTP 메소드의 종류도 제한할 수 있다.
CorsFilter 생성하기
Access-Control 을 확인할 수 있도록 커스텀 Filter 를 생성하는 방식이다.
@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() {
}
}
* Request headers (클라이언트의 요청 헤더)
- Origin: 요청을 보내는 페이지의 도메인
- Access-Control-Request-Method: 실제 요청하려는 메소드 종류
- Access-Control-Request-Headers: 실제 요청에 포함되어 있는 헤더 이름
* Response headers (서버에서의 응답 헤더)
- Access-Control-Allow-Origin: 요청을 허용하는 출처. * 인 경우, 모든 도메인의 요청을 허용한다.
- Access-Control-Allow-Methods: 요청을 허용하는 메소드 종류. 헤더 값에 해당하는 메소드만 접근 허용한다. (default : GET, POST)
- Access-Control-Allow-Headers: 요청을 허용하는 헤더 이름
- Access-Control-Max-Age: 클라이언트에서 preflight 의 요청 결과를 저장할 시간(sec). 해당 시간동안은 preflight요청을 다시 하지 않게 된다.
프록시 서버 호스팅 & 구축
프록시 서버는 클라이언트가 프록시 서버 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다.
쉽게 말해 브라우저와 서버 간의 통신을 도와주는 중계서버이다.
https://cors-anywhere.herokuapp.com
대표적으로 heroku Proxy 서버가 있다.
이 서버를 사용하면 중간에 요청을 가로채서 Access-Control-Allow-Origin : * 를 설정해서 응답해준다.
axios({
method: "GET"
url: `https://cors-anywhere.herokuapp.com/https://api.dropper.tech/covid19/status/korea?
locale=${city}`,
headers: {
'APIKey': COVID_APIKEY,
},
})
요청해야 하는 URL앞에 프록시 서버 URL을 붙여서 요청하게 되면,
클라이언트에서 서버로 리소스를 요청할 때 발생하는 Cors 문제를 아주 간단하게 해결할 수 있다.
출처
https://evan-moon.github.io/2020/05/21/about-cors/
https://steady-coding.tistory.com/616
https://devlog.kro.kr/postitem/?name=Cors#proxy
'Computer Science > 네트워크' 카테고리의 다른 글
웹 통신의 흐름 (0) | 2023.03.13 |
---|---|
TCP와 UDP (1) | 2023.03.12 |
네트워크 계층 구조 (0) | 2023.03.12 |
REST의 정의와 HTTP 메소드 (0) | 2023.03.10 |
HTTP와 HTTPS (0) | 2023.03.09 |