๐Ÿ’ Spring/Spring Security

CORS์ด๋ž€ ๋ฌด์—‡์ด๊ณ , Spring-boot์—์„œ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•

iseunghan 2022. 12. 27. 22:29
๋ฐ˜์‘ํ˜•

CORS (Cross-Origin Resource Sharing) ์ด๋ž€?

A๋ผ๋Š” ๋„๋ฉ”์ธ์—์„œ ์ œ๊ณต๋˜๋Š” FE์—์„œ → B๋ผ๋Š” ๋„๋ฉ”์ธ์œผ๋กœ ์ œ๊ณต๋˜๋Š” BE์— HTTP ์š”์ฒญ์„ ํ–ˆ์„ ๊ฒฝ์šฐ, ๋ธŒ๋ผ์šฐ์ €๋Š” ์ด๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด๋ผ ํŒ๋‹จํ•˜๊ณ  ๊ทธ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ์„ ๊ธˆ์ง€ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

Preflight Request

๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹ค์ œ HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ ์ „ ๋ธŒ๋ผ์šฐ์ € ์Šค์Šค๋กœ ์ด ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•œ์ง€ ์˜ˆ๋น„ ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ ์ด๊ฒŒ ๋ฐ”๋กœ Preflight Request๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด Preflight Request๋Š” OPTION ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ์š”์ฒญํ•˜๋Š”๋ฐ ์„œ๋ฒ„์—์„œ ๋ณด๋‚ด์ค€ ์‘๋‹ต ํ—ค๋”์— Access-Controller-* ํ—ค๋”๋“ค์ด ์ž˜ ๊ตฌ์„ฑ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  1. GET, POST, HEAD ์š”์ฒญ์ธ์ง€ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.
  2. Custom-Header๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.
  3. Content-Type์ด ํ‘œ์ค€ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  4. ๋งŒ์•ฝ Simple Request๋ผ๊ณ  ํŒ๋‹จ์ด ๋˜์—ˆ๋‹ค๋ฉด → ์‹ค์ œ ์š”์ฒญ์„ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
  5. ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด, Prefilght Request๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ Custom-Header์™€ Content-Type์ด ํ•จ๊ป˜ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.
  6. ์„œ๋ฒ„์—์„œ ์‘๋‹ตํ•ด์ค€ Access-Control-* ํ—ค๋”๋“ค๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ด ์š”์ฒญ์ด ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ์‹ค์ œ ์š”์ฒญ์„ ์ „์†ก, ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด CORS ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

Simple Request๋Š” Preflight๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ์š”์ฒญ์ด Preflight๋ฅผ ํŠธ๋ฆฌ๊ฑฐ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Simple Request๋ฅผ ์ œ์™ธํ•œ ์š”์ฒญ์—๋งŒ preflight๊ฐ€ ๋™์ž‘ํ•˜๋Š”๋ฐ์š”, ๋‹ค์Œ ์กฐ๊ฑด๋“ค์„ ๋งŒ์กฑํ•˜๋Š” ์š”์ฒญ์„ Simple Request๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

  • GET, HEAD, POST ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์š”์ฒญ
  • User-Agent๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •ํ•œ ํ—ค๋” ์™ธ ์ˆ˜์ •์œผ๋กœ ์„ค์ •ํ•œ ํ—ค๋”๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (๋‹ค์Œ ๊ฐ’๋“ค๋งŒ ํ—ˆ์šฉ, ๊ทธ ์™ธ ๊ฐ’๋“ค์€ Simple Request๊ฐ€ ์•„๋‹ˆ๋‹ค.)
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

Simple Request ์™ธ ์š”์ฒญ๋“ค์€ ๋ชจ๋‘ OPTION ๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ๋ฆฌ์†Œ์Šค๋กœ HTTP ์š”์ฒญ์„ ๋ณด๋‚ด ์‹ค์ œ ์š”์ฒญ์ด ์•ˆ์ „ํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ Preflight๋ผ๊ณ  ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Preflight + ์‹ค์ œ ์š”์ฒญ์— ๋Œ€ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค

  • Preflight ์š”์ฒญ์—๋งŒ Access-Control-Request-* ํ—ค๋”๊ฐ€ ํฌํ•จ๋˜๊ณ  ์‹ค์ œ POST ์š”์ฒญ์—๋Š” ํฌํ•จ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Access-Control-Request-Method: POST → ์‹ค์ œ ์š”์ฒญ์€ POST ๋ฉ”์†Œ๋“œ๋กœ ์ „์†กํ•  ๊ฒƒ์ด๋ผ๊ณ  ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  • Access-Control-Request-Headers: X-PINGOTHER, Content-type → ์‹ค์ œ ์š”์ฒญ์— ์ปค์Šคํ…€ ํ—ค๋”๋ฅผ ํฌํ•จํ•ด ์ „์†กํ•  ๊ฒƒ์ž„์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  • ์ด์ œ ์„œ๋ฒ„๋Š” ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ ์š”์ฒญ์„ ์ˆ˜๋ฝํ• ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Access-Control-* Header ์ข…๋ฅ˜ ๋ฐ ์„ค๋ช…

  • Access-Control-Allow-Origin: ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” origin์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. “*”๋Š” ๋ชจ๋“  origin์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Access-Control-Allow-Methods: ์›๋ณธ ๊ฐ„ ์š”์ฒญ์— ํ—ˆ์šฉ๋˜๋Š” HTTP ๋ฉ”์†Œ๋“œ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
  • Access-Control-Allow-Headers: Cross-Origin Requests์— ๋Œ€ํ•ด ํ—ˆ์šฉ๋œ ์š”์ฒญ ํ—ค๋”๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • Access-Control-Expose-Headers: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ํ—ค๋”๋ฅผ ์„œ๋ฒ„์˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • Access-Control-Max-Age: preflight request ์š”์ฒญ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ๊ฐ„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • Access-Control-Allow-Credentials: credentials ํ”Œ๋ž˜๊ทธ๊ฐ€ true์ผ ๋•Œ, ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์ด ํ—ค๋”๊ฐ€ ์—†์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‘๋‹ต์„ ๋ฌด์‹œํ•˜๊ณ  ์›น ์ปจํ…์ธ ๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ๋Š” ์„œ๋น„์Šค ๋‹จ๊ณ„๊ฐ€ ์•„๋‹Œ ๊ฐœ๋ฐœ ๋‹จ๊ณ„์ด๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„์—์„œ CORS๋ฅผ ์ „์ฒด ํ—ˆ์šฉํ•ด์ฃผ๋„๋ก ์„ค์ •ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ ํ™˜๊ฒฝ

FE(React) → Nginx(Proxy pass) → BE(Spring Boot)

  • Nginx: 192.168.0.101:8080 (proxy → 8081)
  • BE: 192.168.0.101:8081

React ์—์„œ 8081๋กœ ์š”์ฒญ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

Access to XMLHttpRequest at 'http://192.168.0.101:8080' from origin 'http://192.168.0.102:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Spring-boot์—์„œ CORS ํ—ˆ์šฉ

4๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. WebMvcConfigurer ์ƒ์† ๋ฐ›์•„ CORS ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ(Security ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ)
  2. ์ปค์Šคํ…€ CorsFilter๋ฅผ ์ƒ์„ฑ
  3. CorsConfigurationSource Bean ์ƒ์„ฑ
  4. Controller ํด๋ž˜์Šค ๋ ˆ๋ฒจ์— @CrossOrigin ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ (๋‹ค๋ฃจ์ง€ ์•Š์„ ์˜ˆ์ •)

1. WebMvcConfigurer ์ƒ์† ๋ฐ›์•„ CORS ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .exposedHeaders("Authorization", "*")
                .maxAge(3000);
    }

}

Security๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ ์„ค์ •์œผ๋กœ ์ ํ•ฉํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

2. CorsFilter ๋นˆ์œผ๋กœ ๋“ฑ๋ก

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class SimpleCORSFilter implements Filter {

private final Logger log = LoggerFactory.getLogger(SimpleCORSFilter.class);

public SimpleCORSFilter() {
    log.info("SimpleCORSFilter init");
}

@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", request.getHeader("Origin"));
    response.setHeader("Access-Control-Allow-Credentials", "true");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Max-Age", "3600");
    response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
    chain.doFilter(req, res);
}

@Override
public void init(FilterConfig filterConfig) {
}

@Override
public void destroy() {
}

}

์ถ”๊ฐ€๋กœ Security ์„ค์ • ํŒŒ์ผ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors()
            ...
        return http.build();
}

์œ„ ์ฝ”๋“œ ํ•˜๋‚˜๋กœ ์–ด๋–ป๊ฒŒ ์šฐ๋ฆฌ์˜ ํ•„ํ„ฐ๊ฐ€ ์ถ”๊ฐ€๊ฐ€ ๋˜๋Š”์ง€๋Š” ์•„๋ž˜์—์„œ ์ž์„ธํžˆ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค.

3. CorsConfigurationSource Bean ์ƒ์„ฑ

Spring Security ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors()
            ...
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
}

์œ„์—์„œ ์„ค์ •ํ•œ ์ฝ”๋“œ๋“ค์ด ์–ด๋–ค์‹์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๊ธฐ

httpSecurity.cors()

httpSecurity.cors()๋ฅผ ์ ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  1. corsFilter๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋นˆ์ด ๋“ฑ๋ก์ด ๋˜์–ด์žˆ์œผ๋ฉด ํ•ด๋‹น CorsFilter๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ทธ๋ ‡์ง€ ์•Š๊ณ , corsConfigurationSource๊ฐ€ ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ์„ค์ •์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  3. ๊ทธ๋ ‡์ง€ ์•Š๊ณ , Spring MVC๊ฐ€ ํด๋ž˜์Šค ํŒจ์Šค์— ์žˆ๋Š” ๊ฒฝ์šฐ HandlerMappingIntrospector๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

CorsConfigurationSource๋ฅผ Bean์œผ๋กœ ๋“ฑ๋ก

CorsConfigurationSource๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๊ฒŒ ๋˜๋ฉด cors() ๋ฉ”์†Œ๋“œ์—์„œ ์„ค๋ช…ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ 2๋ฒˆ์งธ ์กฐ๊ฑด์— ๋งŒ์กฑํ•˜๊ฒŒ ๋˜์–ด ์„ค์ •์„ ์ ์šฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ๋“ฑ๋กํ•œ CorsConfigurationSource๊ฐ€ ์ž˜ ๋“ฑ๋ก๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด๋ ค๋ฉด CorsFilter์— ๋””๋ฒ„๊น… ์ง€์ ์œผ๋กœ ์„ค์ •ํ•˜๊ณ  ์‚ดํŽด๋ณด๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

package org.springframework.web.filter;

public class CorsFilter extends OncePerRequestFilter {

    private final CorsConfigurationSource configSource;

    private CorsProcessor processor = new DefaultCorsProcessor();

    /**
     * Constructor accepting a {@link CorsConfigurationSource} used by the filter
     * to find the {@link CorsConfiguration} to use for each incoming request.
     * @see UrlBasedCorsConfigurationSource
     */
    public CorsFilter(CorsConfigurationSource configSource) {
        Assert.notNull(configSource, "CorsConfigurationSource must not be null");
        this.configSource = configSource; // ** ์ด ์ง€์ .
    }

    ...
}

์šฐ๋ฆฌ๊ฐ€ ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์„ค์ •ํ•ด์คฌ๋˜ ConfigurationSource๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋“ฑ๋ก๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Nginx์—์„œ CORS ํ—ˆ์šฉ

๋งŒ์•ฝ Nginx๋ฅผ ์›น์„œ๋ฒ„๋กœ ์‚ฌ์šฉ์ค‘์ด๋ผ๋ฉด, Spring-boot์—์„œ CORS๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด๋„ CORS ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์„ค์ •์„ ๊ตฌ์„ฑํ•˜์—ฌ CORS๋ฅผ ํ—ˆ์šฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

server {
        listen 8081 default_server;
        listen [::]:8081 default_server;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                proxy_hide_header Access-Control-Allow-Origin;

                                # preFlight ์š”์ฒญ ํ—ˆ์šฉ
                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' '*';
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, OPTIONS';
                        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
                        add_header 'Access-Control-Max-Age' 86400;
                        return 204;
                }

                                # ๊ทธ ์™ธ ์š”์ฒญ์— ๋Œ€ํ•ด์„œ ํ—ค๋” ๋…ธ์ถœ
                if ($request_method != 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' '*' always;
                        add_header 'Content-Type' 'application/json' always;
                }

                proxy_pass http://localhost:8082;
        }
    ...
}

 

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

 

REFERENCES

https://docs.spring.io/spring-security/reference/6.0.0/servlet/integrations/cors.html

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-security-filters

https://www.baeldung.com/spring-security-cors-preflight

https://greeng00se.tistory.com/119

https://jay-ji.tistory.com/72

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

https://evan-moon.github.io/2020/05/21/about-cors/

๋ฐ˜์‘ํ˜•