0. CORS
๊ฐ๋ฐํ ๋ ์ ๋ง ๋ง์ด ๋ง์ฃผ์น๊ฒ ๋๋ CORS๋ฅผ ์ ๋๋ก ์์๋ณด์.
CORS (Cross-Origin Resource Sharing)
HTTP ํค๋๋ฅผ ์ด์ฉํ์ฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ํ ์์์ ๋ค๋ฅธ ์ถ์ฒ์์ ์ ๊ทผํ ์ ์๋๋ก ๊ถํ์ ๋ถ์ฌํ๋ ์ ์ฑ .
์ฌ๊ธฐ์ '์ถ์ฒ'๋, ๋๋ฉ์ธ, ํ๋กํ ์ฝ, ํฌํธ๋ฅผ ๋ปํ๋ค.
์์์ ํธ์ถํ ํด๋ผ์ด์ธํธ์ ์์์ ๋ด๋ฑ์ ์ฌ์ดํธ๊ฐ ๋๋ฉ์ธ, ํ๋กํ ์ฝ, ํฌํธ๊ฐ ๋ค๋ฅด๋ค๋ฉด "๋ธ๋ผ์ฐ์ " ์์ ๋ง๋๋ค๋ ๊ฒ์ด๋ค.
๋ง์ฝ, CORS ์ ์ฑ ์ด ์๋ค๋ฉด..
ํดํน ์๋๋ฆฌ์ค #1
1. ์ฌ์ฉ์๊ฐ ์ด๋ค ํดํน์ฉ ์ฌ์ดํธ์ ์ ์ํ๋ค.
2. ํด๋น ํดํน์ฉ ์ฌ์ดํธ์์ ์คํ๋๋ js์์๋ ์ฌ์ฉ์ ๋ธ๋ผ์ฐ์ ์ ์๋ ์ฟ ํค, ์ธ์ ๋ฑ์ ์ด์ฉํ์ฌ ๋ค๋ฅธ ์ฌ์ดํธ์ ์ธ์ฆ, ์ธ๊ฐ๋ฅผ ์๋ํ๋ค.
3. ํดํน์ฉ ์ฌ์ดํธ๋ CORS ์ ์ฑ ์ด ์๋ค๋ฉด ๋ค๋ฅธ ์ฌ์ดํธ์ ๋ฆฌ์์ค๋ฅผ ์ป๋๋ค.
๋ธ๋ผ์ฐ์ ๊ฐ ๋ง์์ฃผ๋ ์์ ํ ์ ์ฑ ์ด๋ค ^_^
ํ ๋ง๋๋ก ์ ๋ฆฌํ์๋ฉด,
์๋ฒ ์ ์ฅ์์ ์ฃผ์, ํ๋กํ ์ฝ, ํฌํธ๊ฐ ๋ฌ๋ผ๋ ๋ฆฌ์์ค๋ฅผ ํ์ฉํ ๊ฑด๋ฐ,
์ด๋ ๋๋ฉ์ธ์์ ํด๋น ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํด๋ ๋๋์ง?๋ฅผ ํ์ฉํ๋ ์ ์ฑ .
0-1. SOP(Same-Origin Policy)
๋์ผ ์ถ์ฒ ์ ์ฑ ์ด๋ผ๊ณ ๋ ํ๋ฉฐ, ๊ฐ์ ์ถ์ฒ์์๋ง ๋ฆฌ์์ค๋ฅผ ๊ณต์ ํ๋ ๊ท์น์ ๊ฐ์ง๋ค.
์ต๊ทผ์ ๊ฑฐ์ REST API๋ฅผ ์ด์ฉํ๊ธฐ ๋๋ฌธ์ ์ฌ๋งํ๋ฉด ๋๋ฉ์ธ์ด ๋ค๋ฅธ ๊ฑด ์ด์ฉ ์ ์๋ค.
๊ฐ์ฅ ์์ ํ ๋ฐฉ๋ฒ์ด์ง๋ง ์ด์ CORS ์ ์ฑ ์ ์งํจ ๋ฆฌ์์ค๋ฅผ ํ์ฉํด์ผ ํ๋ค.
1. Preflight
Preflight๋ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์๊ฒ ๋ฏธ๋ฆฌ ํ์ฉ ๊ฐ๋ฅํ์ง ํ์ธํ๋ ๋ฐฉ๋ฒ์ด๋ค.
ํด๋ผ์ด์ธํธ๊ฐ OPTIONS Method๋ก ๋จผ์ ์๋ฒ์๊ฒ ๋ฆฌ์์ค ์์ฒญ์ด ๊ฐ๋ฅํ์ง ๋ฌป๊ฒ๋๋ค.
preflight ๋ฐฉ์์ผ๋ก OPTIONS ๋ฉ์๋๋ก ์์ฒญ์ ํ ์๊ฐ์ด๋ค.
Request Header๋ ๋ค์๊ณผ ๊ฐ๋ค.
- Origin (์ถ์ฒ) : ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ๋ณด๋ด๋ ๋ณธ์ธ์ ์ถ์ฒ.
- Access-Control-Request-Method : ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ๋ณด๋ด๋ HTTP ๋ฉ์๋.
- Access-Control-Request-Headers : ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ๋ณด๋ด๋ ํค๋.
๊ทธ์ ํด๋นํ Response Header๋ฅผ ์ดํด๋ณด์.
- Access-Control-Allow-Credentials : ์๊ฒฉ ์ฆ๋ช (์ฟ ํค, ํ ํฐ ๋ฑ)์ ํฌํจํ ์์ฒญ์ ํ์ฉํ ์ง ์ฌ๋ถ.
true ๋๋ false.
ํด๋น ํค๋๊ฐ true๋ก ์ค์ ๋ ๊ฒฝ์ฐ, Access-Control-Allow-Origin์ wildcard(*)๋ฅผ ์ฌ์ฉํ ์ ์๊ณ ๋ช ์์ ์ธ ์ถ์ฒ๋ฅผ ์ง์ ํด์ผ ํจ.
์ค์ ๋ก, credentials๋ฅผ true๋ก ์ค์ ํ๊ณ Allow Origin์ ์์ผ๋์นด๋(*)๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ง์์ ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋์ง๋ค.
java.lang.IllegalArgumentException: When allowCredentials is true,
allowedOrigins cannot contain the special value "*"
since that cannot be set on the "Access-Control-Allow-Origin" response header.
To allow credentials to a set of origins,
list them explicitly or consider using "allowedOriginPatterns" instead.
- Access-Control-Allow-Headers : ํด๋น ๋๋ฉ์ธ์ ๋ํด ์๋ฒ๊ฐ ํ์ฉํ๋ ํค๋.
- Access-Control-Allow-Methods : ํด๋น ๋๋ฉ์ธ์ ๋ํด ์๋ฒ๊ฐ ํ์ฉํ๋ HTTP ๋ฉ์๋.
- Access-Control-Allow-Origin : ์๋ฒ์์ ํ์ฉํ Origin(์ถ์ฒ)
- max-age : Preflight๋ ์์ฒญ์ ๋ ๋ฒ ๋ณด๋ด๋ฉด์ ๋ฆฌ์์ค๋ฅผ ๋ง์ด ์ฌ์ฉํ๊ฒ ๋๋๋ฐ, ์ด๋ฅผ ๊ด๋ฆฌํ๊ณ ์ ๋ธ๋ผ์ฐ์ ๋ ์ฒซ ์์ฒญ์ ์บ์๋ฅผ ์ ์ฅํด๋
๋ค. ์๋ฒ๋ ์ด ์บ์๋ฅผ ์ ์ฅํ ์๊ฐ์ ์ง์ ํด์ค๋ค.
2. Simple Reqeust
Simple Request ๋ฐฉ์์ Preflight๋ฅผ ์ ํํ์ง ์๊ณ ์์ฒญ์ ์ฆ์ ์ ์กํ๋ค.
ํ์ง๋ง ์กฐ๊ฑด์ด ์๋ค.
1. HTTP ๋ฉ์๋๋ ๋ฐ๋์ GET, POST, HEAD์ฌ์ผ ํ๋ค.
2. Content-Type์ ๋ค์ ์ค ํ๋์ฌ์ผ ํ๋ค.
- application/x-www-form-unlencoded
- multipart/form-data
- text/plain
๋ณดํต ์ธ๊ฐ๋ฅผ ์ํ Authorization์ด๋, REST API์ฉ application/json์ ์ ์ํ ์ ์๋ค.
๋ฐ๋ผ์ ์ผ๋ถ ๊ฒฝ์ฐ์๋ง ์ฐ์ธ๋ค.
3. Reqeust ํค๋๋ ๋ค์๋ง ํ์ฉ๋๋ค.
- Accept
- Accept-Langugage
- Content-Language
- Content-Type
3. CorsConfig
๊ทธ๋ ๋ค๋ฉด ์ด์ Spring Boot์์ Cors๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์.
*Spring Boot 3, Spring Security 6.x.x ๊ธฐ์ค
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CorsConfig {
public static CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//๋ฆฌ์์ค๋ฅผ ํ์ฉํ URL ์ง์
ArrayList<String> allowedOriginPatterns = new ArrayList<>();
allowedOriginPatterns.add("http://localhost:5000");
allowedOriginPatterns.add("http://127.0.0.1:5000");
configuration.setAllowedOrigins(allowedOriginPatterns);
//ํ์ฉํ๋ HTTP METHOD ์ง์
ArrayList<String> allowedHttpMethods = new ArrayList<>();
allowedHttpMethods.add("GET");
allowedHttpMethods.add("POST");
allowedHttpMethods.add("PUT");
allowedHttpMethods.add("DELETE");
configuration.setAllowedMethods(allowedHttpMethods);
configuration.setAllowedHeaders(Collections.singletonList("*"));
// configuration.setAllowedHeaders(List.of(HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE));
//์ธ์ฆ, ์ธ๊ฐ๋ฅผ ์ํ credentials ๋ฅผ TRUE๋ก ์ค์
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
๋จผ์ , ๋๋ CorsConfig ์ ์ฑ ํด๋์ค๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ๊ณ config๋ฅผ returnํ๋ static ๋ฉ์๋๋ฅผ ๋ง๋ค์๋ค.
๊ฐ ํ์ฉํ ํจํด๋ค์ ๋ณดํต String ๊ฐ์ผ๋ก ์ฃผ์ ํ๋ฉด ๋๋ค.
CORS ์ค์ ํด๋์ค ํ๋๋ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ๋์ด ์๋ค.
public class CorsConfiguration {
@Nullable
private List<String> allowedOrigins;
@Nullable
private List<OriginPattern> allowedOriginPatterns;
@Nullable
private List<String> allowedMethods;
@Nullable
private List<HttpMethod> resolvedMethods = DEFAULT_METHODS;
@Nullable
private List<String> allowedHeaders;
@Nullable
private List<String> exposedHeaders;
@Nullable
private Boolean allowCredentials;
@Nullable
private Boolean allowPrivateNetwork;
@Nullable
private Long maxAge;
//...
}
ํ์ํ ์ค์ ์ ์ฃผ์ ํด๋๊ณ , Security Filter Chain์ ์ค์ ์ ๋ฑ๋กํด๋๋ฉด ๋๋ค.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
//...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CORS ์ ์ฑ
์ค์
http
.cors(cors -> cors
.configurationSource(CorsConfig.corsConfigurationSource())
);
}
}
๋ง์ฝ ์ฌ๊ธฐ์ ๊ฒฝ๋ก๋ณ ๋ค๋ฅธ CORS๋ฅผ ์ค์ ํ๊ณ ์ถ๋ค๋ฉด, ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํ ์๋ ์๋ค.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
@Order(0)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.cors((cors) -> cors
.configurationSource(apiConfigurationSource())
)
...
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain myOtherFilterChain(HttpSecurity http) throws Exception {
http
.cors((cors) -> cors
.configurationSource(myWebsiteConfigurationSource())
)
...
return http.build();
}
CorsConfigurationSource apiConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://api.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
CorsConfigurationSource myWebsiteConfigurationSource() {
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;
}
}
์ฌ๊ธฐ์ @Order๋ฅผ ํตํด ๋น์ ๋ก๋ ์์๋ฅผ ์ ์ํ ์ ์๋ค.
@Order ์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ๊ฐ์ LOWEST_PRECEDENCE์ด๋ค.
Order์ ๋ฎ์ ๊ฐ์ด ๋์ ์ฐ์ ์์๋ฅผ ๊ฐ๋๋ฐ, LOWEST_PRECEDENCE๋ Integer์ MAX๊ฐ์ด๋ค.
์ฆ, ๊ธฐ๋ณธ๊ฐ์ ๊ฐ์ฅ ๋ฎ์ ์ฐ์ ์์์ด๋ค.
๋ง์ฝ ๋์ผํ ์์ ๊ฐ์ ๊ฐ์ง ๊ฐ์ฒด๋ค์ด๋ฉด ์์์ ์ ๋ ฌ ์์น๋ฅผ ๊ฐ๊ฒ ๋๋ค.
๋ฐ๋ผ์, ์ ์์๋ api ์๋ํฌ์ธํธ ๋ถ๋ถ์ ์ฐ์ ์์๋ฅผ ๋๊ฒ(0) ์ก์์ ๋น์ ๋จผ์ ๋ก๋๋๋๋ก ํ๋ค.
๋ฒ์ธ
CORS๋ ๋ธ๋ผ์ฐ์ ์์ฒด์์ ๋ง๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์, ๋ธ๋ผ์ฐ์ ์ค์ ์ ํตํด ์ ์ฑ ์ ๋นํ์ฑํ ํ ์ ์๋ค.
Chrome Extension ์ค CORS Unblock์ ์ด์ฉํ๋ฉด ์์๋ก CORS๋ฅผ ์ผ๊ณ ๋ ์ ์๋ค.
https://chromewebstore.google.com/detail/cors-unblock/lfhmikememgdcahcdlaciloancbhjino?hl=ko
CORS๋ ๋ณด์์ ์ง์ผ์ฃผ๋ ๊ณ ๋ง์ด ์ ์ฑ ์ด๋ ํ ์คํธ์ฉ์ด ์๋๋ผ๋ฉด ๊ผญ ๊บผ๋์ ^_^
๋ ์์ธํ ๋ด์ฉ
https://ddonghyeo.tistory.com/58
ํด๋น ๊ธ์์๋ CORS๊ฐ ์ ํํ ์ด๋ค ํํฐ๋ฅผ ํตํด ๋์ํ๊ณ , ์ด๋ป๊ฒ ๊ฒ์ฌ๋ฅผ ์งํํ๋์ง ์ ์ ์๋ค.