跨域的相关知识请参考https://blog.csdn.net/weixin_66375317/article/details/124545878。SpringMVC解决跨域的方法请参考https://blog.csdn.net/forlinkext/article/details/121267500。
SpringMVC可通过配置mvc:cors解决跨域。
<mvc:cors>
<mvc:mapping allowed-origins="*" path="/*"></mvc:mapping>
</mvc:cors>
allowed-origins表示允许的请求来源,path表示访问路径。
一、跨域环境搭建和解析mvc:cors
跨域一般在前后端分离中比较常见。新建html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="http://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
function println(data) {
console.log(data);
console.log('print');
}
function jsonp_test() {
$.ajax({
type: "get",
url: "http://localhost:8090/myself-web/corsTest",
dataType: "text",
success:function(result){
console.log('success:',result)
},
error:function(err){
console.log('错误:',err)
},
});
}
</script>
</head>
<body onl oad="jsonp_test()">
</body>
</html>
用浏览器打开页面时通过ajax请求后端服务。此时不是同一个端口,发生跨域。
mvc:cors标签由CorsBeanDefinitionParser解析。
CorsBeanDefinitionParser.parse(Element element, ParserContext parserContext)
public BeanDefinition parse(Element element, ParserContext parserContext) {
Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
List<Element> mappings = DomUtils.getChildElementsByTagName(element, "mapping");
if (mappings.isEmpty()) {
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
corsConfigurations.put("/**", config);
}
else {
for (Element mapping : mappings) {
CorsConfiguration config = new CorsConfiguration();
if (mapping.hasAttribute("allowed-origins")) {
String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ",");
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
}
if (mapping.hasAttribute("allowed-origin-patterns")) {
String[] patterns = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origin-patterns"), ",");
config.setAllowedOriginPatterns(Arrays.asList(patterns));
}
if (mapping.hasAttribute("allowed-methods")) {
String[] allowedMethods = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-methods"), ",");
config.setAllowedMethods(Arrays.asList(allowedMethods));
}
if (mapping.hasAttribute("allowed-headers")) {
String[] allowedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-headers"), ",");
config.setAllowedHeaders(Arrays.asList(allowedHeaders));
}
if (mapping.hasAttribute("exposed-headers")) {
String[] exposedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("exposed-headers"), ",");
config.setExposedHeaders(Arrays.asList(exposedHeaders));
}
if (mapping.hasAttribute("allow-credentials")) {
config.setAllowCredentials(Boolean.parseBoolean(mapping.getAttribute("allow-credentials")));
}
if (mapping.hasAttribute("max-age")) {
config.setMaxAge(Long.parseLong(mapping.getAttribute("max-age")));
}
config.applyPermitDefaultValues();
config.validateAllowCredentials();
corsConfigurations.put(mapping.getAttribute("path"), config);
}
}
MvcNamespaceUtils.registerCorsConfigurations(
corsConfigurations, parserContext, parserContext.extractSource(element));
return null;
}
遍历mvc:cors下的mvc:mapping子标签。解析allowed-origins,allowed-origin-patterns,allowed-methods,allowed-headers,exposed-headers,allow-credentials,max-age等属性封装成CorsConfiguration。将mvc:mapping标签的path属性值为key,CorsConfiguration为value加到corsConfigurations中。applyPermitDefaultValues设置CorsConfiguration属性的默认值。
applyPermitDefaultValues()
public CorsConfiguration applyPermitDefaultValues() {
if (this.allowedOrigins == null && this.allowedOriginPatterns == null) {
this.allowedOrigins = DEFAULT_PERMIT_ALL;
}
if (this.allowedMethods == null) {
this.allowedMethods = DEFAULT_PERMIT_METHODS;
this.resolvedMethods = DEFAULT_PERMIT_METHODS
.stream().map(HttpMethod::resolve).collect(Collectors.toList());
}
if (this.allowedHeaders == null) {
this.allowedHeaders = DEFAULT_PERMIT_ALL;
}
if (this.maxAge == null) {
this.maxAge = 1800L;
}
return this;
}
默认的允许源是DEFAULT_PERMIT_ALL(*),允许所有来源。默认的支持请求方法是GET,HEAD,POST。默认请求头是ALL。默认的最大age是1800秒。
CROS相关的OPTION方法预检请求请参考https://blog.csdn.net/small_cutey/article/details/125313478。
二、SpringMVC源码解析CORS
在RequestMappingHandlerMapping中获取handle时,即AbstractHandlerMethodMapping.lookupHandlerMethod(String lookupPath, HttpServletRequest request)查找HandlerMethod,如果找到了handle则调用addMatchingMappings(directPathMatches, matches, request);匹配的handle。最终会调用RequestMappingInfo.getMatchingCondition(HttpServletRequest request):
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
会判断是否支持请求方法。
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
都会判断是否是CORS pre-flight。如果是则进行处理。
RequestMethodsRequestCondition.getMatchingCondition(HttpServletRequest request)
public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) {
if (CorsUtils.isPreFlightRequest(request)) {
return matchPreFlight(request);
}
if (getMethods().isEmpty()) {
if (RequestMethod.OPTIONS.name().equals(request.getMethod()) &&
!DispatcherType.ERROR.equals(request.getDispatcherType())) {
return null; // We handle OPTIONS transparently, so don't match if no explicit declarations
}
return this;
}
return matchRequestMethod(request.getMethod());
}
CorsUtils.isPreFlightRequest判断是否是OPTION预检请求,如果调用matchPreFlight从请求头Access-Control-Request-Method获取请求方法,进行判断是否支持请求方法。如果不是OPTION预检请求且是OPTION请求方法返回null。否则调用matchRequestMethod匹配请求方法。
CorsUtils.isPreFlightRequest(HttpServletRequest request)
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);
}
请求方法是OPTIONS且Origin和Access-Control-Request-Method请求头不为null才是CORS pre-flight。
AbstractHandlerMapping.getHandler(HttpServletRequest request)方法中getHandlerExecutionChain(handler, request)获取到拦截器链之后:
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
if (getCorsConfigurationSource() != null) {
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
config = (globalConfig != null ? globalConfig.combine(config) : config);
}
if (config != null) {
config.validateAllowCredentials();
}
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
hasCorsConfigurationSource判断HandleMapping是否有CROS配置;CorsUtils.isPreFlightRequest判断是否是CORS pre-flight。如果HandleMapping是否有CROS配置或者是CORS pre-flight则获取CROS配置并设置cors拦截器。如果corsConfigurationSource不为null,从corsConfigurationSource获取cros配置并且和已有的CorsConfiguration合并。springmvc配置文件中的mvc:cors配置加载到corsConfigurationSource中。
AbstractHandlerMethodMapping.getCorsConfiguration(Object handler, HttpServletRequest request)
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
}
else {
CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);
corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
}
}
return corsConfig;
}
1、从父类获取CROS配置
2、如果handle是HandlerMethod,且handlerMethod不等于PREFLIGHT_AMBIGUOUS_MATCH,则从mappingRegistry获取cros配置且和父类获取的cros配置合并。
AbstractHandlerMapping.getCorsConfiguration(Object handler, HttpServletRequest request)
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
Object resolvedHandler = handler;
if (handler instanceof HandlerExecutionChain) {
resolvedHandler = ((HandlerExecutionChain) handler).getHandler();
}
if (resolvedHandler instanceof CorsConfigurationSource) {
return ((CorsConfigurationSource) resolvedHandler).getCorsConfiguration(request);
}
return null;
}
从handle中获取CROS配置。获取不到返回null。
AbstractHandlerMapping.getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config)
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(0, new CorsInterceptor(config));
return chain;
}
}
1、如果是CORS pre-flight则设置PreFlightHandler为handle。
2、否则添加CorsInterceptor拦截器到首位。
如果是OPTION预检请求,则会调用PreFlightHandler处理请求。处理完OPTION预检请求后才是实际的请求。此时会走下面的分支。会添加CorsInterceptor。
PreFlightHandler.handleRequest(HttpServletRequest request, HttpServletResponse response)
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
corsProcessor.processRequest(this.config, request, response);
}
DefaultCorsProcessor.processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,HttpServletResponse response)
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
return true;
}
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
1、获取Vary请求头并判断Vary请求头是否包含Origin,Access-Control-Request-Method,Access-Control-Request-Headers请求头,如果包含则设置相应的请求头到response中。
2、CorsUtils.isCorsRequest判断是否存在跨域,不存在返回true。
3、如果有Access-Control-Allow-Origin请求头返回true。
4、如果cros配置为空且是OPTION预检请求rejectRequest拒绝请求,设置响应状态码为403。如果存在跨域且没有设置CROS则会拒绝请求。
5、否则调用handleInternal处理请求
CorsUtils.isCorsRequest(HttpServletRequest request)
public static boolean isCorsRequest(HttpServletRequest request) {
String origin = request.getHeader(HttpHeaders.ORIGIN);
if (origin == null) {
return false;
}
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
String scheme = request.getScheme();
String host = request.getServerName();
int port = request.getServerPort();
return !(ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) &&
ObjectUtils.nullSafeEquals(host, originUrl.getHost()) &&
getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort()));
}
通过请求协议,域名和端口判断是否存在跨域。返回false表示不存在跨域。true表示存在跨域。
DefaultCorsProcessor.handleInternal(ServerHttpRequest request, ServerHttpResponse response,CorsConfiguration config, boolean preFlightRequest)
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
1、checkOrigin检查Origin请求头
2、若Origin请求头为空,表示不允许来源访问。rejectRequest拒绝请求。
3、checkMethods检查请求方法,若不支持请求方法调用rejectRequest拒绝请求。
4、checkHeaders检查请求头,如果是OPTION预检请求且不支持请求头调用rejectRequest拒绝请求。
5、否则允许该来源访问。将allowedOrigin设置到Access-Control-Allow-Origin响应头
6、设置Access-Control-Allow-Headers,Access-Control-Expose-Headers,Access-Control-Allow-Credentials,Access-Control-Max-Age等响应头。
通过上面可知OPTION预检请求判断请求是否存在跨域,是否存在cros配置,是否存在Origin请求头。如果不存在跨域返回true。
如果存在跨域,
- cros配置不支持Origin请求头
- cros配置不支持请求方法,请求头
如果存在以上情况则会拒绝请求。
如果cros支持跨域的请求来源,则会设置Access-Control-Allow-Origin,Access-Control-Allow-Headers,Access-Control-Expose-Headers,Access-Control-Allow-Credentials,Access-Control-Max-Age等响应头。
如果设置CorsInterceptor拦截器,在DispatcherServlet.doDispatch(HttpServletRequest request, HttpServletResponse response)
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
执行实际handle之前执行preHandle方法。
CorsInterceptor.preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// Consistent with CorsFilter, ignore ASYNC dispatches
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult()) {
return true;
}
return corsProcessor.processRequest(this.config, request, response);
}
1、如果存在异步web则返回true。
2、否则调用corsProcessor.processRequest。corsProcessor为DefaultCorsProcessor。上面以看过。如果corsProcessor.processRequest返回false,则不会执行实际handle处理请求。