ecsimsw

Spring boot의 CORS 설정 본문

Spring boot의 CORS 설정

JinHwan Kim 2021. 6. 13. 18:31

Blocked by CORS policy

클라이언트 어플리케이션에서 서버로 요청을 보낼 때 만난 문제이다. CORS 정책에 의해서 요청이 제한되었다는데 CORS가 무엇인지, 어떻게 해결했는지 설명하려고 한다.

 

Access to XMLHttpRequest at 'http://localhost:8080/members/login' 
from origin 'http://localhost:3000' has been blocked by CORS policy
: Response to preflight request doesn't pass access control check
: No 'Access-Control-Allow-Origin' header is present on the requested resource.

에러 코드

 

 

###### HTTP Request ######
OPTIONS /members/login/ HTTP/1.1
host: localhost:8080
connection: keep-alive
accept: */*
access-control-request-method: POST
access-control-request-headers: content-type
origin: http://localhost:3000
sec-fetch-mode: cors
sec-fetch-site: same-site
sec-fetch-dest: empty
referer: http://localhost:3000/

의도하지 않은 OPTIONS 요청

 

CORS 이전에 SOP

CORS를 설명하기 앞서, SOP라는 개념을 알아야 한다. SOP는 Same origin policy의 약자로 이름 그대로 같은 출처에 대한 HTTP 요청만을 허락한다는 정책이다.

 

이런 정책이 왜 필요할까? SOP가 없다면 어떻게 될까? 예를 하나 들어보자.

 

해커가 본인의 api 서버(https://hackers.com)를 하나 열어두고 접근한 사용자로 하여금 해커가 정의한 요청들(https://mail.goole.com)을 수행하도록 하는 스크립트를 짜둔다. 그리곤 링크를 교묘하게 숨겨 뿌리는 것으로 사용자가 본인 서버에 접근할 수 있도록 한다.

 

사용자는 본인의 브라우저의 인증 쿠키와 함께 해커가 정의한 요청을 수행하게 되고, 'https://mail.goole.com' 입장에서는 사용자가 요청한 것과 같으므로 정상 수행하게 된다.

 

 

 

 

위 공격 시나리오를 SOP가 잡는다. 위 그림의 3번, 사용자가 'https://hackers.com' 로부터 'https://mail.goole.com' 으로 보내는 요청에서 다른 출처에 대한 요청임을 확인하고 제한하는 것으로 이런 공격을 막을 수 있게 된다.

 

CORS란 

이런 SOP, 동일 출처 정책이 보안을 위해 중요하지만 다른 출처로 요청을 보내고 자원을 얻어야 하는 상황이 분명히 있다. 반대로 서버 입장에서도 다른 출처에서 본인의 자원을 얻을 수 있도록 열어줘야 하는 상황이 존재한다.

 

Cross-Origin Resource Sharing, 교차 출처 리소스 공유이 바로 이런 상황에서 다른 출처의 자원에 접근할 수 있는 권한을 부여하는 체제이다. CORS 처리 방식 중에 가장 보편적인, 그리고 이번에 내가 경험했던 Prefilght request를 이용하는 방법을 소개하려고 한다.

 

Preflight request 방식은 브라우저가 본 요청을 보내기 전에 preflight에 해당하는 사전 요청을 미리 보내 서버에 어떤 요청이 전달될 것임을 알리고, 서버에서 허용한 정책을 확인하여 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 방법이다.

 

OPTIONS /members/login/ HTTP/1.1
host: localhost:8080

access-control-request-method: POST
access-control-request-headers: content-type
origin: http://localhost:3000

 

위처럼 localhost:3000에서, POST :: localhost:8000/member/login을 fetch로 보내기 전 preflight 요청이 전송된다. OPTIONS 메서드로 보내진 요청 안에는 실제 요청이 어떤 메소드인지, 헤더에 어떤 것이 포함되어 있는지, 출처가 어디에 있는지가 포함된다.

 

Access-Control-Allow-Headers: authorization
Access-Control-Allow-Methods: GET,HEAD,POST
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: Authorization
Access-Control-Max-Age: 1800

 

그리고 거기에 대한 응답으로 서버 측에서 허용한 자원 정책, 예시에선 허용한 헤더, 메서드, 출처, 캐시 시간을 포함하게 된다.

 

 

스프링부트에서 CORS 매핑 설정하기

WebMvcConfigurer를 상속한 설정 파일에서 addCorsMapping을 재정의하는 것으로 CORS 매핑을 설정할 수 있다. 다음은 내가 addCorsMapping을 설정하면서 발생한 문제 사항과 해결했던 방법이다.

 

@Configuration
class WebConfig :WebMvcConfigurer{
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("*")
    }
}

 

위 예시의 경우에는 모든 매핑에, 모든 출처에 해당하는 요청을 모두 처리하겠다는 의미가 된다. 이렇게 다 열었다고 생각했는데, 다음과 같은 에러를 만났다. 출처를 *으로 열어줘선 안된다로 이해하고 필요한 출처만을 지정했다.

 

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

 

@Configuration
class WebConfig :WebMvcConfigurer{
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:3000")
    }
}

 

이번엔 이런 에러를 만났다. 'Access-Control-Allow-Credentials' header는 요청 시 자격 증명이 필요함을 표시한다고 한다. true를 응답할 경우 클라이언트는 실제 요청에서 쿠키, authorization 헤더, TLS 클라이언트 인증서 등으로 자격 증명을 해야한다.

 

The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true'

 

@Configuration
class WebConfig :WebMvcConfigurer{
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:8080", "http://localhost:3000")
            .allowCredentials(true)
    }
}

 

login(loginInfo) {
   return axios.post(BASE_URL + 'members/login/', loginInfo, { withCredentials: true });
}

 

 

드디어 로그인 요청 (members/login)에 성공하였다. 그런데...

 

 

이번에는 헤더에 값이 안넘어간다. Authorization을 key로 분명 토큰 값을 넘겼는데...

 

@PostMapping("/login")
  fun login(@RequestBody loginRequest: Member, response:HttpServletResponse): ResponseEntity<Void> {
  response.addHeader("authorization", authService.login(loginRequest))
  return ResponseEntity.ok(memberService.findByName(loginRequest.name))
}

알아보니 CORS 때문에 아래 기본적인 몇개 헤더만 넘길 수 있다고 한다. 아래처럼 노출될 헤더 선택.

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

authorization 헤더를 넘기 위해 exposedHeaders 조건을 추가했다.

 

@Configuration
class WebConfig :WebMvcConfigurer{
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:8080", "http://localhost:3000")
            .allowCredentials(true)
            .exposedHeaders("authorization")
    }
}

 

프론트단은 이런 느낌이다.

 

login(loginInfo) {
  axios.post(BASE_URL + 'member/login/', loginInfo, { withCredentials: true })
  .then(res => {
    localStorage.setItem("authorization", res.headers["authorization"])
    this.setState({
      user: res.data
    })
  })
}
getAccessibleRooms() {
  const auth_token = "Bearer "+ localStorage.getItem("authorization")
  return axios({
      method: 'get',
      url: BASE_URL + 'rooms/accessible',
      headers :{
        Authorization: auth_token,
      },
      withCredentials: true,
  });
}

 

더 나아가면..

1. XSS, CSRF

 : 가장 쉽게는 XSS는 클라이언트를, CSRF는 서버를 더럽힌다. XSS는 페이지에 스크립트를 숨기고 사용자로 하여금 해당 스크립트를 실행하도록 하여 사용자의 쿠키나 세션 정보를 훔치는 해킹 기술이다. 반면 CSRF는 사용자의 브라우저의 쿠키나 세션 정보로 서버에 마치 인증된 사용자가 요청하는 것처럼 공격 명령을 처리한다. 

 

XSS는 사용자가 특정 웹 사이트를 신용하는 점을 노린 것이라면, CSRF는 사이트가 사용자의 웹 브라우저를 신뢰한다는 점을 노린다.

 

2. XSS와 HTTP Only option

 : HTTP Only option의 쿠키를 이용, 사용자의 쿠키를 자바스크립트로 꺼내지 못하게 함으로써 XSS Cookie hijacking risk를 감소시킬 수 있다.

 

3. CSRF와 SOP

 : 동일 출처의 요청이 아닌 경우 리소스 반환을 막는 것으로, 또는 라우팅을 막는 것으로 CSRF 위험을 줄일 수 있다.

 

4. SOP와 CORS

 : SOP를 사용하는 것이 보안상 좋은 것은 사실이지만, 분명 출처가 다른 상황에서 요청을 보내야하고, 응답을 줘야하는 상황이 있다. 그 권한을 부여하는 체제, 과정이 CORS이다.

 

5. 출처

 : 프로토콜, 포트, 호스트가 같을 때 동일 출처라고 말한다. 아래는 Mozilla의 예시이다.

 

http://store.company.com/dir/page.html와 출처 비교

http://store.company.com/dir2/other.html 성공 경로만 다름
http://store.company.com/dir/inner/another.html 성공 경로만 다름
https://store.company.com/secure.html 실패 프로토콜 다름
http://store.company.com:81/dir/etc.html 실패 포트 다름 (http://는 80이 기본값)
http://news.company.com/dir/other.html 실패 호스트 다름

 

Comments