์œ„์™€ ๊ฐ™์€ ์งค์˜ ๋‚ด์šฉ์ฒ˜๋Ÿผ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๊ณ ํ†ต์„ ์ฃผ๋Š” ๊ฒƒ์€ CORS ์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ CORS๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ์˜ ์ •์ฑ…์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์€ ์—†์œผ๋ฉฐ ๋ธŒ๋ผ์šฐ์ €์—์„œ์˜ CORS ๋งค์ปค๋‹ˆ์ฆ˜์„ ์ดํ•ดํ•˜๊ณ  ์„œ๋ฒ„ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผํ•  ๋ถ€๋ถ„์ด๋‹ค. MDN์˜ Preflight Request ๋ฌธ์„œ์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ์ฒ˜๋Ÿผ ๋Œ€๋ถ€๋ถ„์˜ CORS ๋ฌธ์ œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ์˜ XHR ์š”์ฒญ์— ์˜ํ•ด ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ(Preflight Request)์— ์˜ํ•œ CORS ์œ„๋ฐฐ ์‘๋‹ต์„ ๋ฐ›๊ณ  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ œํ•œํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•œ๋‹ค.

ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ

ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์€ Origin ํ—ค๋”์™€ Access-Control-Request-Method ํ—ค๋” ๊ทธ๋ฆฌ๊ณ  OPTIONS ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜ํ–‰๋œ๋‹ค.

HTTP
OPTIONS /resource/foo Access-Control-Request-Method: DELETE Access-Control-Request-Headers: origin, x-requested-with Origin: https://foo.bar.org

์ฐธ๊ณ ๋กœ, ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ CorsUtils์— ์˜ํ•ด ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ์กฐ๊ฑด์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„๋˜์–ด์žˆ๋‹ค.

Java
public abstract class CorsUtils { public static boolean isPreFlightRequest(HttpServletRequest request) { return (HttpMethod.OPTIONS.matches(request.getMethod()) && request.getHeader(HttpHeaders.ORIGIN) != null && request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null); } }

์Šคํ”„๋ง CORS ๋””๋ฒ„๊ทธ

CORS ์š”์ฒญ์— ๋Œ€ํ•œ ๊ฒ€์ฆ์€ CorsFilter์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋„๋ก ๊ตฌํ˜„๋œ DefaultCorsProcessor์— ์˜ํ•ด ์ˆ˜ํ–‰๋œ๋‹ค. CORS ์š”์ฒญ์— ์˜ํ•ด ์œ„๋ฐฐ๋˜๋Š” ์ƒํ™ฉ์— ๋Œ€ํ•ด์„œ ์›์ธ์„ ๋กœ๊ทธ๋กœ ์ถœ๋ ฅํ•ด๋ณด๋ ค๋Š” ๊ฒฝ์šฐ DefaultCorsProcessor์— ๋Œ€ํ•œ ๋กœ๊ทธ ๋ ˆ๋ฒจ์„ TRACE ๋˜๋Š” DEBUG๋กœ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” CorsFilter ๋˜๋Š” DefaultCorsProcessor์˜ ์ฝ”๋“œ ๋ผ์ธ์— ์ค‘๋‹จ์ ์„ ๊ฑธ์–ด์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋กœ๊ทธ ๋ ˆ๋ฒจ๋กœ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

application.yml
logging.level: org.springframework.web.cors.DefaultCorsProcessor: TRACE

CORS์˜ ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์„ ์ดํ•ดํ•œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž

์Šคํ”„๋ง MockMvc๋กœ CORS ํ…Œ์ŠคํŠธ

์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉ์ค‘์ด๋ฉฐ CorsConfiguration ์„ค์ •์„ ํ•ด๋‘์—ˆ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด MockMvc๋ฅผ ํ™œ์šฉํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

@Test
@DisplayName("Preflight request")
void TestPreflightRequest() {
    Assertions.assertDoesNotThrow(() -> {
        List<String> allowedOrigins = corsConfiguration.getAllowedOrigins();
        if (allowedOrigins == null) {
            allowedOrigins = new ArrayList<>();
        }

        List<String> allowedMethods = corsConfiguration.getAllowedMethods();
        if (allowedMethods == null) {
            allowedMethods = new ArrayList<>();
        }

        mockMvc.perform(options("/")
                .header("Origin", allowedOrigins)
                .header("Access-Control-Request-Method", "GET")
            )
            .andExpect(status().isOk())
            .andExpect(header().stringValues("Access-Control-Allow-Origin", allowedOrigins.toArray(new String[]{})))
            .andExpect(header().string("Access-Control-Allow-Methods", String.join(",", allowedMethods)))
            .andDo(print());
    });
}

cURL๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•

ํฌ์ŠคํŠธ๋งจ ๋„๊ตฌ๋กœ HTTP ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ cURL๋กœ๋„ ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค. ํฌ์ŠคํŠธ๋งจ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ OPTIONS๋ฅผ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค. DefaultCorsProcessor์— ์˜ํ•ด CORS์— ์œ„๋ฐฐ๋œ ์ƒํ™ฉ์ด ์žˆ๋‹ค๋ฉด Invalid CORS request์ด๋ผ๋Š” ์‘๋‹ต๊ณผ ํ•จ๊ป˜ 403 ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ ํ™•์ธ ๋  ๊ฒƒ์ด๋‹ค.

Windows Terminal
PS C:\Users\Mambo> curl -X OPTIONS 'http://localhost:5000' -H 'Origin: http://localhost' -H 'Access-Control-Request-Method: GET' -v * Trying 127.0.0.1:5000... * Connected to localhost (127.0.0.1) port 5000 (#0) > OPTIONS / HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/8.0.1 > Accept: */* > Origin: http://localhost > Access-Control-Request-Method: GET > < HTTP/1.1 403 Forbidden < Expires: 0 < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < X-XSS-Protection: 0 < Pragma: no-cache < X-Frame-Options: DENY < Date: Sat, 22 Jul 2023 12:56:00 GMT < Connection: keep-alive < Vary: Origin < Vary: Access-Control-Request-Method < Vary: Access-Control-Request-Headers < X-Content-Type-Options: nosniff < Transfer-Encoding: chunked < Invalid CORS request* Connection #0 to host localhost left intact

Postman์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•

HTTP ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” ํฌ์ŠคํŠธ๋งจ์—์„œ ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Origin ํ—ค๋”๋ฅผ ํฌํ•จํ•˜๋ฉด ๋œ๋‹ค. ํฌ์ŠคํŠธ๋งจ์—์„œ ์•Œ์•„์„œ OPTIONS๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๋งŒ์•ฝ, CORS๋Š” ํ”„๋ก ํŠธ์—”๋“œ ์˜์—ญ์—์„œ ํ•ด๊ฒฐํ•ด์•ผํ•  ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์žˆ๋‹ค๋ฉด CORS์— ๋Œ€ํ•ด์„œ ๋‹ค์‹œ ํ•™์Šตํ•˜๊ธธ ๋ฐ”๋ž€๋‹ค.
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ CORS์˜ ๊ณ ํ†ต์—์„œ ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ๋„๋ก ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋Š” ์ฑ…์ž„์„ ๋‹คํ•ด์•ผ ํ•  ๊ฒƒ์ด๋‹ค.