cross-origin HTTP 요청들을 제한함.cross-origin요청을 하려면 서버의 동의가 필요함.CORS(Cross-Origin Resource Sharing)라고 함.Origin을 담아 전달.
Access-Control-Allow-Origin을 담아 클라이언트로 반환함.
Origin을 비교해서 검증에 실패하면 CORS 에러가 발생하고, 위 이미지와 같이 Origin:http://localhost:3000으로 동일할 경우 cross-origin의 리소스를 문제없이 가져올 수 있게됨.CorsFilter
package org.apache.catalina.filters;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.Globals;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.RequestUtil;
import org.apache.tomcat.util.http.ResponseUtil;
import org.apache.tomcat.util.http.parser.MediaType;
import org.apache.tomcat.util.res.StringManager;
/**
* <p>
* A {@link jakarta.servlet.Filter} that enable client-side cross-origin requests by implementing W3C's CORS
* (<b>C</b>ross-<b>O</b>rigin <b>R</b>esource <b>S</b>haring) specification for resources. Each
* {@link HttpServletRequest} request is inspected as per specification, and appropriate response headers are added to
* {@link HttpServletResponse}.
* </p>
* <p>
* By default, it also sets following request attributes, that help to determine the nature of the request downstream.
* </p>
* <ul>
* <li><b>cors.isCorsRequest:</b> Flag to determine if the request is a CORS request. Set to <code>true</code> if a CORS
* request; <code>false</code> otherwise.</li>
* <li><b>cors.request.origin:</b> The Origin URL, i.e. the URL of the page from where the request is originated.</li>
* <li><b>cors.request.type:</b> Type of request. Possible values:
* <ul>
* <li>SIMPLE: A request which is not preceded by a pre-flight request.</li>
* <li>ACTUAL: A request which is preceded by a pre-flight request.</li>
* <li>PRE_FLIGHT: A pre-flight request.</li>
* <li>NOT_CORS: A normal same-origin request.</li>
* <li>INVALID_CORS: A cross-origin request which is invalid.</li>
* </ul>
* </li>
* <li><b>cors.request.headers:</b> Request headers sent as 'Access-Control-Request-Headers' header, for pre-flight
* request.</li>
* </ul>
* If you extend this class and override one or more of the getXxx() methods, consider whether you also need to override
* {@link CorsFilter#doFilter(ServletRequest, ServletResponse, FilterChain)} and add appropriate locking so that the
* {@code doFilter()} method executes with a consistent configuration.
*
* @see <a href="http://www.w3.org/TR/cors/">CORS specification</a>
*/
public class CorsFilter extends GenericFilter {
private static final long serialVersionUID = 1L;
private static final StringManager sm = StringManager.getManager(CorsFilter.class);
private transient Log log = LogFactory.getLog(CorsFilter.class); // must not be static
/**
* A {@link Collection} of origins consisting of zero or more origins that are allowed access to the resource.
*/
private final Collection<String> allowedOrigins = new HashSet<>();
/**
* Determines if any origin is allowed to make request.
*/
private boolean anyOriginAllowed;
/**
* A {@link Collection} of methods consisting of zero or more methods that are supported by the resource.
*/
private final Collection<String> allowedHttpMethods = new HashSet<>();
/**
* A {@link Collection} of headers consisting of zero or more header field names that are supported by the resource.
*/
private final Collection<String> allowedHttpHeaders = new HashSet<>();
/**
* A {@link Collection} of exposed headers consisting of zero or more header field names of headers other than the
* simple response headers that the resource might use and can be exposed.
*/
private final Collection<String> exposedHeaders = new HashSet<>();
/**
* A supports credentials flag that indicates whether the resource supports user credentials in the request. It is
* true when the resource does and false otherwise.
*/
private boolean supportsCredentials;
/**
* Indicates (in seconds) how long the results of a pre-flight request can be cached in a pre-flight result cache.
*/
private long preflightMaxAge;
/**
* Determines if the request should be decorated or not.
*/
private boolean decorateRequest;
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) {
throw new ServletException(sm.getString("corsFilter.onlyHttp"));
}
// Safe to downcast at this point.
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// Determines the CORS request type.
CorsFilter.CORSRequestType requestType = checkRequestType(request);
// Adds CORS specific attributes to request.
if (isDecorateRequest()) {
decorateCORSProperties(request, requestType);
}
switch (requestType) {
case SIMPLE:
// Handles a Simple CORS request.
case ACTUAL:
// Handles an Actual CORS request.
this.handleSimpleCORS(request, response, filterChain);
break;
case PRE_FLIGHT:
// Handles a Pre-flight CORS request.
this.handlePreflightCORS(request, response, filterChain);
break;
case NOT_CORS:
// Handles a Normal request that is not a cross-origin request.
this.handleNonCORS(request, response, filterChain);
break;
default:
// Handles a CORS request that violates specification.
this.handleInvalidCORS(request, response, filterChain);
break;
}
}
@Override
public void init() throws ServletException {
parseAndStore(getInitParameter(PARAM_CORS_ALLOWED_ORIGINS, DEFAULT_ALLOWED_ORIGINS),
getInitParameter(PARAM_CORS_ALLOWED_METHODS, DEFAULT_ALLOWED_HTTP_METHODS),
getInitParameter(PARAM_CORS_ALLOWED_HEADERS, DEFAULT_ALLOWED_HTTP_HEADERS),
getInitParameter(PARAM_CORS_EXPOSED_HEADERS, DEFAULT_EXPOSED_HEADERS),
getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS, DEFAULT_SUPPORTS_CREDENTIALS),
getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE, DEFAULT_PREFLIGHT_MAXAGE),
getInitParameter(PARAM_CORS_REQUEST_DECORATE, DEFAULT_DECORATE_REQUEST));
}
......
protected CORSRequestType checkRequestType(final HttpServletRequest request) {
CORSRequestType requestType = CORSRequestType.INVALID_CORS;
if (request == null) {
throw new IllegalArgumentException(sm.getString("corsFilter.nullRequest"));
}
String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN);
// Section 6.1.1 and Section 6.2.1
if (originHeader != null) {
if (originHeader.isEmpty()) {
requestType = CORSRequestType.INVALID_CORS;
} else if (!RequestUtil.isValidOrigin(originHeader)) {
requestType = CORSRequestType.INVALID_CORS;
} else if (RequestUtil.isSameOrigin(request, originHeader)) {
return CORSRequestType.NOT_CORS;
} else {
String method = request.getMethod();
if (method != null) {
if ("OPTIONS".equals(method)) {
String accessControlRequestMethodHeader =
request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
if (accessControlRequestMethodHeader != null && !accessControlRequestMethodHeader.isEmpty()) {
requestType = CORSRequestType.PRE_FLIGHT;
} else if (accessControlRequestMethodHeader != null &&
accessControlRequestMethodHeader.isEmpty()) {
requestType = CORSRequestType.INVALID_CORS;
} else {
requestType = CORSRequestType.ACTUAL;
}
} else if ("GET".equals(method) || "HEAD".equals(method)) {
requestType = CORSRequestType.SIMPLE;
} else if ("POST".equals(method)) {
String mediaType = MediaType.parseMediaTypeOnly(request.getContentType());
if (mediaType == null) {
requestType = CORSRequestType.SIMPLE;
} else {
if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES.contains(mediaType)) {
requestType = CORSRequestType.SIMPLE;
} else {
requestType = CORSRequestType.ACTUAL;
}
}
} else {
requestType = CORSRequestType.ACTUAL;
}
}
}
} else {
requestType = CORSRequestType.NOT_CORS;
}
return requestType;
}
....
package jakarta.servlet;
import java.io.Serializable;
import java.util.Enumeration;
/**
* Provides a base class that implements the Filter and FilterConfig interfaces to reduce boilerplate when writing new
* filters.
*
* @see jakarta.servlet.Filter
* @see jakarta.servlet.FilterConfig
*
* @since Servlet 4.0
*/
public abstract class GenericFilter implements Filter, FilterConfig, Serializable {
private static final long serialVersionUID = 1L;
/**
* The filter configuration.
*/
private volatile FilterConfig filterConfig;
@Override
public String getInitParameter(String name) {
return getFilterConfig().getInitParameter(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return getFilterConfig().getInitParameterNames();
}
/**
* Obtain the FilterConfig used to initialise this Filter instance.
*
* @return The config previously passed to the {@link #init(FilterConfig)} method
*/
public FilterConfig getFilterConfig() {
return filterConfig;
}
@Override
public ServletContext getServletContext() {
return getFilterConfig().getServletContext();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
init();
}
/**
* Convenience method for sub-classes to save them having to call <code>super.init(config)</code>. This is a NO-OP
* by default.
*
* @throws ServletException If an exception occurs that interrupts the Filter's normal operation
*/
public void init() throws ServletException {
// NO-OP
}
@Override
public String getFilterName() {
return getFilterConfig().getFilterName();
}
}
GenericFilter라는 추상 클래스를 상속 받아 구현되어 있음.GenericFilterBean라는 추상 클래스를 상속 받아 구현.CorsFilter 클래스 내부에 있는 doFilter() 메서드
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) {
throw new ServletException(sm.getString("corsFilter.onlyHttp"));
}
// Safe to downcast at this point.
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// Determines the CORS request type.
CorsFilter.CORSRequestType requestType = checkRequestType(request);
// Adds CORS specific attributes to request.
if (isDecorateRequest()) {
decorateCORSProperties(request, requestType);
}
switch (requestType) {
case SIMPLE:
// Handles a Simple CORS request.
case ACTUAL:
// Handles an Actual CORS request.
this.handleSimpleCORS(request, response, filterChain);
break;
case PRE_FLIGHT:
// Handles a Pre-flight CORS request.
this.handlePreflightCORS(request, response, filterChain);
break;
case NOT_CORS:
// Handles a Normal request that is not a cross-origin request.
this.handleNonCORS(request, response, filterChain);
break;
default:
// Handles a CORS request that violates specification.
this.handleInvalidCORS(request, response, filterChain);
break;
}
}
@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();
}
UrlBasedCorsConfigurationSource 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;
}
UrlBasedCorsConfigurationSource 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;
}
}
OR
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
return configuration;
}
}));
return http.build();
}