Post

cors 기록

CORS 를 왜 쓰는가?

동일한 출처(= Origin) 서버에 있는 자원과는 상호 작용을 막는다.
출처 관계 없이 자유롭게 모든 상호작용이 가능하다면, 자바스크립트를 통해 쿠키에서 개인 정보를 빼돌려서 남의 소중한 정보를 훔쳐가는 것이 쉬워진다.

image

image

URL 중에서 Origin 만 기억하면 되는데, Origin 은 Protocol + Host + Port 까지를 말한다.

image

이러한 작업은 서버가 하는 것이 아니라 크롬, 엣지, 파이어폭스 등의 브라우저가 맡는다. 사용하는 브라우저에서 헤더를 분석해서 허용할 지, 차단할 지 여부를 결정한다.

CORS 기본 동작

  1. 클라이언트에서 HTTP 요청의 header 에 Origin 전달
  2. 요청을 받은 서버가 응답 header 에 Access-Control-Allow-Origin 을 담아서 클라이언트로 전달
  3. 본 요청 이전에 연결의 상태를 확인하기 위한 Preflight 가 발생한다. 브라우저가 실제 데이터를 전송하기 전에 해당 요청이 안전한 지 확인하는 것이다. (HTTP 메서드 중 OPTIONS 이 사용된다.)

image

문제 상황 파악

image

image

경고 메시지를 읽어보니, SameSite 옵션이 없어 크롬 브라우저의 기본 정책인 Lax 로 설정되었다고 한다. Lax 는 동일한 도메인만을 허용하는 옵션이기 때문에, SameSite 옵션을 None 으로 주어야 한다.

2021년 크롬 정책은 Lax 로 간주하도록 변경되었으며, https 요청만을 허용한다. 따라서 서버에서 Secure 옵션을 부여해야하며 프론트와 백엔드 모두 https 통신이 필요하다.

인증된 요청

자격 인증 정보는 쿠키나 Authorization 헤더에 포함된 토큰 등을 말한다. 자격 인증 정보를 주고 받을 때 credentials 옵션이 필요하다. 만약 옵션 설정이 없다면 브라우저는 기본적으로 인증 관련 데이터는 전송하지 않도록 동작한다.

  • same-origin (default)
    • 같은 출처 간 요청에만 인증 정보를 담는다.
  • include
    • 모든 요청에 인증 정보를 담는다.
  • omit
    • 모든 요청에 인증 정보를 담지 않는다.

적용 방법은 아래와 같다.

프론트에서의 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// fetch 메서드
fetch("https://example.com:1234/users/login", {
	method: "POST",
	credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
    body: JSON.stringify({
        userId: 1,
    }),
})

// axios 라이브러리
axios.post('https://example.com:1234/users/login', { 
    profile: { username: username, password: password } 
}, { 
	withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})

// jQuery 라이브러리
$.ajax({
	url: "https://example.com:1234/users/login",
	type: "POST",
	contentType: "application/json; charset=utf-8",
	dataType: "json",		
	xhrFields: { 
    	withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
    },
	success: function (retval, textStatus) {
		console.log( JSON.stringify(retval));
	}
});

백엔드 서버에서의 설정

인증 요청에 관해서는 다른 CORS 설정과 조금 다르게 해준다.

  • 응답 헤더의 Access-Control-Allow-Credentials 항목을 true
  • 응답 헤더의 Access-Control-Allow-Origin 에 와일드 카드(*) 를 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Methods 에 와일드 카드(*) 를 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Headers 에 와일드 카드(*) 를 사용할 수 없다.

모든 값을 다 설정할 필요는 없고, 아래와 같이 CORS 연관된 HTTP 헤더 값만 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용함.
# * 이면 모든 곳에 공개되어 있음을 의미한다. 
Access-Control-Allow-Origin : https://naver.com

# 리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더
Access-Control-Request-Methods : GET, POST, PUT, DELETE

# 요청을 허용하는 해더.
Access-Control-Allow-Headers : Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization

# 클라이언트에서 preflight 의 요청 결과를 저장할 기간을 지정
# 60초 동안 preflight 요청을 캐시하는 설정으로, 첫 요청 이후 60초 동안은 OPTIONS 메소드를 사용하는 예비 요청을 보내지 않는다.
Access-Control-Max-Age : 60

# 클라이언트 요청이 쿠키를 통해서 자격 증명을 해야 하는 경우에 true. 
# 자바스크립트 요청에서 credentials가 include일 때 요청에 대한 응답을 할 수 있는지를 나타낸다.
Access-Control-Allow-Credentials : true

# 기본적으로 브라우저에게 노출이 되지 않지만, 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정
Access-Control-Expose-Headers : Content-Length

스프링을 사용하는 경우 아래 설정을 참고한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        	.allowedOrigins("http://localhost:8080", "http://localhost:8081") // 허용할 출처
            .allowedMethods("GET", "POST") // 허용할 HTTP method
            .allowCredentials(true) // 쿠키 인증 요청 허용
            .maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
    }
}


// 특정 컨트롤러에만 CORS 적용하고 싶을때.
@Controller
@CrossOrigin(origins = "*", methods = RequestMethod.GET) 
public class customController {

	// 특정 메소드에만 CORS 적용 가능
    @GetMapping("/url")  
    @CrossOrigin(origins = "*", methods = RequestMethod.GET) 
    @ResponseBody
    public List<Object> findAll(){
        return service.getAll();
    }
}

프록시 서버 중 Nginx 를 사용한다면,

nginx.conf 파일 안에 다음 설정을 추가한다.

1
2
3
4
5
6
location / {
    root html;
    add_header 'Access-Control-Allow-Origin' '*';
    index  index.html index.htm;
}

AWS - S3 호스팅인 경우

  1. S3 콘솔에 들어가 버킷을 선택
  2. 권한 선택
  3. CORS 창에서 Edit
  4. 텍스트 상자에 JSON CORS 규칙 입력
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
  {
    "AllowedHeaders": [
      "Authorization"
    ],
    "AllowedMethods": [
      "GET",
      "HEAD"
    ],
    "AllowedOrigins": [
      "http://www.example.com"
    ],
    "ExposeHeaders": [
      "Access-Control-Allow-Origin"
    ]
  }
]

인증 서버에서 토큰을 획득한 후, 다시 프론트로 전송할 때 쿠키를 사용한다. 하지만! 이 때도 역시나 CORS 문제가 터진다.

프론트엔드

withCredentials 옵션을 활성하 시켜주어야만 인증 정보를 쿠키에 담아서 보낼 수 있다.

1
2
3
4
5
6
7
8
9
// 1. 전역설정
axios.defaults.withCredentials = true;
// 2. 옵션 객체로 추가시키기
axios.post(
    `$URL`,
    {
        withCredentials: true
    }
).then(res => {console.log(res)});

백엔드

크롬에 자격 증명 정보를 쿠키에 담아서 보내기 위해서는, sameSite=None 옵션을 주어야 한다. 이를 위해 Cookie 가 아닌, ResponseCookie 를 사용한다. 인증 정보를 담고 있는 토큰을 응답 헤더에 담아서 보낸다.

1
2
import org.springframework.http.ResponseCookie; # 해당 객체를 사용한다.
import javax.servlet.http.Cookie; # 작동 X
1
2
3
4
5
6
7
8
9
10
11
12
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
    ResponseCookie cookie = ResponseCookie.from(name, value)
        .path("/")
        .sameSite("None")
        .httpOnly(false)
        .secure(true)
        .maxAge(maxAge)
        .build();

    response.addHeader("Set-Cookie", cookie.toString());
}

출처

  1. https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F
  2. https://inpa.tistory.com/entry/AXIOS-%F0%9F%93%9A-CORS-%EC%BF%A0%ED%82%A4-%EC%A0%84%EC%86%A1withCredentials-%EC%98%B5%EC%85%98
  3. https://kindloveit.tistory.com/100
This post is licensed under CC BY 4.0 by the author.