通過JSONP實現跨域已是老生常談,JSONP跨域限制多,最近了解了一下CORS。
參考:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
http://newhtml.net/using-cors/
https://www.w3.org/TR/2013/PR-cors-20131205/
CORS是W3c的一個工作草案,定義了在跨域訪問資源時瀏覽器和服務器應該如何溝通。CORS背后的基本思想是使用自定義的HTTP頭部讓瀏覽器和服務器進行溝通,從而決定請求或響應成功與否。
比如一個簡單的使用get或post發送的請求,它沒有自定義的頭部,而主體內容是text/plain。在發送該請求時,需要給它附加一個額外的Origin頭部,其中包含請求頁面的源信息(協議,域名和端口),以便服務器根據這個頭部信息來決定是否給予相應。
Origin:http://www.nczonline.net
如果服務器認為這個請求可以接受,就在Access-Control-Allow-Origin頭部中回發相同的源信息(如果是公共資源,可以回發***)。
Access-Control-Allow-Origin://www.nczonline.net
如果沒有這個頭部,或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。注意請求和響應都不包含cookies信息。
同源策略:是瀏覽器最核心也最基本的安全功能;同源指的是:同協議,同域名和同端口。精髓:認為自任何站點裝載的信賴內容是不安全的。當被瀏覽器半信半疑的腳本運行在沙箱時,它們應該只被允許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源;參考:JavaScript 的同源策略
JSON & JSONP:JSON 是一種基於文本的數據交換方式,或者叫做數據描述格式。JSONP是資料格式JSON的一種“使用模式”,可以讓網頁從別的網域要資料,由於同源策略,一般來說位於server1.example.com的網頁無法與不是 server1.example.com的服務器溝通,而HTML的script元素是一個例外。利用script元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON資料,而這種使用模式就是所謂的JSONP
對比JSONP和CORS發現以下幾點區別:
- JSONP只能實現GET請求,而CORS支持所有類型的HTTP請求
- 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理
- JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數現代瀏覽器都已經支持了CORS
最近做項目,前端使用vue,后端使用spring-boot,前后端完全分離,開發聯調的時候碰到了跨域問題,用了CORS解決跨域。
查看了spring中幾個cors相關的類:
org.springframework.web.cors.CorsConfiguration
org.springframework.web.servlet.config.annotation.CorsRegistry
org.springframework.web.cors.DefaultCorsProcessor
其中DefaultCorsProcessor源碼如下:
1 /* 2 * Copyright 2002-2016 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.web.cors; 18 19 import java.io.IOException; 20 import java.nio.charset.Charset; 21 import java.util.ArrayList; 22 import java.util.List; 23 import javax.servlet.http.HttpServletRequest; 24 import javax.servlet.http.HttpServletResponse; 25 26 import org.apache.commons.logging.Log; 27 import org.apache.commons.logging.LogFactory; 28 29 import org.springframework.http.HttpHeaders; 30 import org.springframework.http.HttpMethod; 31 import org.springframework.http.HttpStatus; 32 import org.springframework.http.server.ServerHttpRequest; 33 import org.springframework.http.server.ServerHttpResponse; 34 import org.springframework.http.server.ServletServerHttpRequest; 35 import org.springframework.http.server.ServletServerHttpResponse; 36 import org.springframework.util.CollectionUtils; 37 import org.springframework.web.util.WebUtils; 38 39 /** 40 * The default implementation of {@link CorsProcessor}, as defined by the 41 * <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>. 42 * 43 * <p>Note that when input {@link CorsConfiguration} is {@code null}, this 44 * implementation does not reject simple or actual requests outright but simply 45 * avoid adding CORS headers to the response. CORS processing is also skipped 46 * if the response already contains CORS headers, or if the request is detected 47 * as a same-origin one. 48 * 49 * @author Sebastien Deleuze 50 * @author Rossen Stoyanchev 51 * @since 4.2 52 */ 53 public class DefaultCorsProcessor implements CorsProcessor { 54 55 private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); 56 57 private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class); 58 59 60 @Override 61 @SuppressWarnings("resource") 62 public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) 63 throws IOException { 64 65 if (!CorsUtils.isCorsRequest(request)) { 66 return true; 67 } 68 69 ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response); 70 if (responseHasCors(serverResponse)) { 71 logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header"); 72 return true; 73 } 74 75 ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request); 76 if (WebUtils.isSameOrigin(serverRequest)) { 77 logger.debug("Skip CORS processing: request is from same origin"); 78 return true; 79 } 80 81 boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); 82 if (config == null) { 83 if (preFlightRequest) { 84 rejectRequest(serverResponse); 85 return false; 86 } 87 else { 88 return true; 89 } 90 } 91 92 return handleInternal(serverRequest, serverResponse, config, preFlightRequest); 93 } 94 95 private boolean responseHasCors(ServerHttpResponse response) { 96 try { 97 return (response.getHeaders().getAccessControlAllowOrigin() != null); 98 } 99 catch (NullPointerException npe) { 100 // SPR-11919 and https://issues.jboss.org/browse/WFLY-3474 101 return false; 102 } 103 } 104 105 /** 106 * Invoked when one of the CORS checks failed. 107 * The default implementation sets the response status to 403 and writes 108 * "Invalid CORS request" to the response. 109 */ 110 protected void rejectRequest(ServerHttpResponse response) throws IOException { 111 response.setStatusCode(HttpStatus.FORBIDDEN); 112 response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET)); 113 } 114 115 /** 116 * Handle the given request. 117 */ 118 protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, 119 CorsConfiguration config, boolean preFlightRequest) throws IOException { 120 121 String requestOrigin = request.getHeaders().getOrigin(); 122 String allowOrigin = checkOrigin(config, requestOrigin); 123 124 HttpMethod requestMethod = getMethodToUse(request, preFlightRequest); 125 List<HttpMethod> allowMethods = checkMethods(config, requestMethod); 126 127 List<String> requestHeaders = getHeadersToUse(request, preFlightRequest); 128 List<String> allowHeaders = checkHeaders(config, requestHeaders); 129 130 if (allowOrigin == null || allowMethods == null || (preFlightRequest && allowHeaders == null)) { 131 rejectRequest(response); 132 return false; 133 } 134 135 HttpHeaders responseHeaders = response.getHeaders(); 136 responseHeaders.setAccessControlAllowOrigin(allowOrigin); 137 responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN); 138 139 if (preFlightRequest) { 140 responseHeaders.setAccessControlAllowMethods(allowMethods); 141 } 142 143 if (preFlightRequest && !allowHeaders.isEmpty()) { 144 responseHeaders.setAccessControlAllowHeaders(allowHeaders); 145 } 146 147 if (!CollectionUtils.isEmpty(config.getExposedHeaders())) { 148 responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders()); 149 } 150 151 if (Boolean.TRUE.equals(config.getAllowCredentials())) { 152 responseHeaders.setAccessControlAllowCredentials(true); 153 } 154 155 if (preFlightRequest && config.getMaxAge() != null) { 156 responseHeaders.setAccessControlMaxAge(config.getMaxAge()); 157 } 158 159 response.flush(); 160 return true; 161 } 162 163 /** 164 * Check the origin and determine the origin for the response. The default 165 * implementation simply delegates to 166 * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}. 167 */ 168 protected String checkOrigin(CorsConfiguration config, String requestOrigin) { 169 return config.checkOrigin(requestOrigin); 170 } 171 172 /** 173 * Check the HTTP method and determine the methods for the response of a 174 * pre-flight request. The default implementation simply delegates to 175 * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}. 176 */ 177 protected List<HttpMethod> checkMethods(CorsConfiguration config, HttpMethod requestMethod) { 178 return config.checkHttpMethod(requestMethod); 179 } 180 181 private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) { 182 return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod()); 183 } 184 185 /** 186 * Check the headers and determine the headers for the response of a 187 * pre-flight request. The default implementation simply delegates to 188 * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}. 189 */ 190 protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) { 191 return config.checkHeaders(requestHeaders); 192 } 193 194 private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) { 195 HttpHeaders headers = request.getHeaders(); 196 return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<String>(headers.keySet())); 197 } 198 199 }
Spring 中對 CORS 規則的校驗,都是通過委托給 DefaultCorsProcessor實現的。
DefaultCorsProcessor 處理過程如下:
首先 CorsUtils.isCorsRequest(request) 通過request請求頭部是否包含origin頭部來判斷是否是cors請求,若不包含則返回true,若包含則繼續往下執行。
然后 responseHasCors(serverResponse) 判斷response頭部是否已經設置了Access-Control-Allow-Origin,如果已經設置了也不用再進行cors處理,返回true,若沒有設置Access-Control-Allow-Origin則繼續往下執行。
再判斷是否同源,判斷request帶的origin(請求的來源域)和forward(需要請求的資源)否一致(協議,域名,端口號一致),一致則證明不是跨域訪問返回true,不一致則繼續往下執行。
最后檢查是否是預請求,服務端是否有跨域配置,沒有跨域配置且不是預請求則返回true,沒有跨域配置但請求是預請求則拒絕該請求返回false。
若有跨域配置,則檢查請求中origin 是否合法,method 是否合法,header是否合法,如果全部合法,則在 response header中添加響應的字段,並交給負責該請求的類處理,如果不合法,則拒絕該請求。
spring-boot項目中通過繼承org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter,重寫addCorsMappings方法支持跨域
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT") .maxAge(3600); }
另外后端通過swagger提供接口參數說明及測試接口,瀏覽器也需要配置支持跨域,以chrome為例,設置支持跨域方式:
右鍵點擊chrome瀏覽器快捷方式,在屬性窗口中加入如下配置:--args --disable-web-security --user-data-dir=E:\chromeDevData,然后重啟瀏覽器。
會發現瀏覽器上方有如下提示,證明跨域配置生效。