CORS (Cross-Origin Resource Sharing) ์ด๋?
A๋ผ๋ ๋๋ฉ์ธ์์ ์ ๊ณต๋๋ FE์์ → B๋ผ๋ ๋๋ฉ์ธ์ผ๋ก ์ ๊ณต๋๋ BE์ HTTP ์์ฒญ์ ํ์ ๊ฒฝ์ฐ, ๋ธ๋ผ์ฐ์ ๋ ์ด๋ฅผ ์๋ก ๋ค๋ฅธ ๋๋ฉ์ธ์์ ๋ฆฌ์์ค๋ฅผ ๊ณต์ ํ๋ ๊ฒ์ด๋ผ ํ๋จํ๊ณ ๊ทธ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด ํธ์ถ์ ๊ธ์งํ๋ ๊ฒ์ด๋ค.
Preflight Request
๋ธ๋ผ์ฐ์ ์์ ์ค์ HTTP ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ ๋ธ๋ผ์ฐ์ ์ค์ค๋ก ์ด ์์ฒญ์ ๋ณด๋ด๋ ๊ฒ์ด ์์ ํ์ง ์๋น ์์ฒญ์ ํ๊ฒ ๋๋๋ฐ ์ด๊ฒ ๋ฐ๋ก Preflight Request๋ผ๊ณ ํฉ๋๋ค.
์ด Preflight Request๋ OPTION ๋ฉ์๋๋ฅผ ์ด์ฉํด ์์ฒญํ๋๋ฐ ์๋ฒ์์ ๋ณด๋ด์ค ์๋ต ํค๋์ Access-Controller-*
ํค๋๋ค์ด ์ ๊ตฌ์ฑ๋์ด์๋์ง ํ์ธํฉ๋๋ค.
- GET, POST, HEAD ์์ฒญ์ธ์ง ํ์ ํฉ๋๋ค.
- Custom-Header๊ฐ ์กด์ฌํ๋์ง ํ์ ํฉ๋๋ค.
- Content-Type์ด ํ์ค ํ์ ์ธ์ง ํ์ธํฉ๋๋ค.
- ๋ง์ฝ
Simple Request
๋ผ๊ณ ํ๋จ์ด ๋์๋ค๋ฉด → ์ค์ ์์ฒญ์ ์ ์กํฉ๋๋ค. - ๊ทธ๋ ์ง ์๋ค๋ฉด,
Prefilght Request
๋ฅผ ์ ์กํฉ๋๋ค. ์ด๋ Custom-Header์ Content-Type์ด ํจ๊ป ์ ์ก๋ฉ๋๋ค. - ์๋ฒ์์ ์๋ตํด์ค 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๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
- WebMvcConfigurer ์์ ๋ฐ์ CORS ์ค์ ์ค๋ฒ๋ผ์ด๋(Security ์ฌ์ฉํ์ง ์์ ๋)
- ์ปค์คํ CorsFilter๋ฅผ ์์ฑ
- CorsConfigurationSource Bean ์์ฑ
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()
๋ฅผ ์ ์ฉํ๊ฒ ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋์ํ๊ฒ ๋ฉ๋๋ค.
corsFilter
๋ผ๋ ์ด๋ฆ์ผ๋ก ๋น์ด ๋ฑ๋ก์ด ๋์ด์์ผ๋ฉด ํด๋น CorsFilter๋ฅผ ์ฌ์ฉํฉ๋๋ค.- ๊ทธ๋ ์ง ์๊ณ ,
corsConfigurationSource
๊ฐ ๋น์ผ๋ก ๋ฑ๋ก๋์ด ์์ผ๋ฉด ํด๋น ์ค์ ์ ์ ์ฉํฉ๋๋ค. - ๊ทธ๋ ์ง ์๊ณ , 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://developer.mozilla.org/ko/docs/Web/HTTP/CORS
https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request