在本機使用瀏覽器訪問遠程服務器進行開發時,我們經常會碰到這種報錯信息:
XMLHttpRequest cannot load xxxxxxxx. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
瞭解過的人一眼就看出了是出現了跨域問題。
那麼,爲什麼會出現這種情況呢?
出於對瀏覽器安全的考量,你在載入其他網站的資源時會觸發跨域資源共享CORS這一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不同源服務器上的指定的資源。當一個資源從與該資源本身所在的服務器不同的域、協議或端口請求一個資源時,資源會發起一個跨域 HTTP 請求。它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
跨域資源共享(CORS)機制允許 Web 應用服務器進行跨域訪問控制,從而使跨域數據傳輸得以安全進行。現代瀏覽器支持在 API 容器中使用 CORS,以降低跨域 HTTP 請求所帶來的風險。
同源的含義是:A網頁設置的 Cookie等消息,B網頁不能打開,除非這兩個網頁"同源"。所謂"同源"指的是:同協議、同域名、同端口。
如果非同源,以下行爲將會受到限制:
(1) Cookie、LocalStorage 和 IndexDB 無法讀取。
(2) DOM 無法獲得。
(3) AJAX 請求不能發送。
利用Postman進行開發或者後端獲取http數據的話,是不會發送OPTIONS進行請求的,即是不會出現跨域問題。跨域問題只有在瀏覽器纔會出現,javascript等腳本的主動http請求才會出現跨域問題。
一、跨域的兩種請求
而瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時滿足以下兩大條件,就屬於簡單請求。
(1) 請求方法是以下三種方法之一:
- HEAD
- GET
- POST
(2)HTTP的頭信息不超出以下幾種字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不同時滿足上面兩個條件,就屬於非簡單請求。
瀏覽器對這兩種請求的處理,是不一樣的。
二、簡單請求
對於簡單請求,在頭信息之中,增加一個Origin
字段,直接發出CORS請求。
GET /cors HTTP/1.1
Origin: null
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的頭信息中,Origin
字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
如果Origin
指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應(只要有包含正確頭信息即可,此時從狀態碼中無法看出錯誤,需要觀察頭信息)。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin
字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest
的onerror
回調函數捕獲。
如果Origin
指定的域名在許可範圍內,服務器返回的響應,會多出以下幾個頭信息字段。
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-
開頭。
(1)Access-Control-Allow-Origin
必選字段。它的值要麼是請求時Origin
字段的值,要麼是一個*
,表示接受任意域名的請求。
(2)Access-Control-Allow-Credentials
可選字段。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設爲true
,即表示服務器明確許可瀏覽器發送Cookie。
(3)Access-Control-Expose-Headers
該字段可選。CORS請求時,XMLHttpRequest
對象的getResponseHeader()
方法只能拿到6個基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必須在Access-Control-Expose-Headers
裏面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
三、非簡單請求
3.1 預檢請求
對於一些可能對服務器數據有特殊要求或有影響的請求,如 PUT,DELETE 和搭配某些 MIME 類型的 POST 方法(常見的application/json
也是非簡單請求),瀏覽器必須先發送一個“預檢請求”——也就是preflight response
,來確認服務器是否允許該請求,允許的話再真正發送相應的請求。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答覆,瀏覽器纔會發出正式的請求,否則就報錯。
curl -X OPTIONS http://example.org -i
響應報文包含一個首部字段,該字段的值表明了服務器支持的所有 HTTP 方法:
爲了檢測後端服務器是否會出現跨域問題,我們可以使用以下命令模擬瀏覽器的OPTIONS預檢請求:
HTTP/1.1 200
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, x-requested-with, Token
Access-Control-Max-Age: 30
...
我們也可以使用JS腳本來判斷服務器允許跨域:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面代碼中,HTTP請求的方法是PUT
,並且發送一個自定義頭信息X-Custom-Header
。下面是"預檢"請求的HTTP頭信息:
OPTIONS /example.org HTTP/1.1
Origin: http://zeng.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
...
"預檢"請求用的請求方法是OPTIONS
,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin
,表示請求來自哪個源。
除了Origin
字段,"預檢"請求的頭信息包括兩個特殊字段。
(1)Access-Control-Request-Method
必須字段,列出瀏覽器的CORS請求所用到的HTTP方法。
(2)Access-Control-Request-Headers
非必須字段,該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段。
3.2 預檢請求的迴應
服務器收到"預檢"請求以後,檢查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以後,確認允許跨源請求,就可以做出迴應。
以下是服務器同意了預檢請求的返回頭信息:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://zeng.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
....
(1)Access-Control-Allow-Origin
必選字段。它的值要麼是請求時Origin
字段的值,要麼是一個*
,表示接受任意域名的請求。
(2)Access-Control-Allow-Methods
必選字段,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。
(3)Access-Control-Allow-Headers
可選字段。如果瀏覽器請求包括Access-Control-Request-Headers
字段,則Access-Control-Allow-Headers
字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段。
(4)Access-Control-Allow-Credentials
可選字段。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設爲true
,即表示服務器明確許可瀏覽器發送Cookie。
(5)Access-Control-Max-Age
可選字段。用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即允許緩存該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求。
3.3 瀏覽器的正常請求和迴應
從上面可以看出,在服務器中可以使用過濾器進行判斷來源的有效性判斷。
如果服務器通過了"預檢"請求,則會返回正常的狀態碼以及包含CORS相關的頭信息字段,需要的最少信息如下:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://zeng.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
....
如果服務器不通過"預檢"請求,同樣是返回正常的狀態碼,但是不設置任何CORS相關的頭信息字段,這樣瀏覽器便不會發送相應請求,避免了用戶通過其他非法來源訪問服務器,降低用戶出現損失的可能。
HTTP/1.1 200 OK
....
服務器通過了"預檢"請求之後,在預檢請求後的一段有效期內,每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin
頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin
頭信息字段。
四、跨域問題的解決
既然瞭解了跨域問題出現的原因,那麼解決方法的原理也都千篇一律了,都是通過定義響應中的CORS頭信息進行實現的,瀏覽器檢查到OPTIONS的響應頭信息上包含:Access-Control-Allow-Origin
、Access-Control-Allow-Methods
,即代表服務器允許跨域。
那麼有以下幾種跨域實現方式:
過濾器允許跨域使用原生代碼,使用Servlet便可以使用。
CorFilter、WebMvConfigurer、@CrossOrigin 需要 SpringMVC 4.2以上版本才支持,對應於springBoot 1.3版本以上
過濾器跨域、CorFilter、WebMvConfigurer屬於全局 CORS 配置,@CrossOrigin屬於局部 CORS配置。如果使用了局部跨域是會覆蓋全局跨域的規則,所以可以通過 @CrossOrigin 註解來進行細粒度更高的跨域資源控制。
注意:
4.1 使用過濾器允許跨域
通過上文我們可以知道,如果要允許跨域,只需要在瀏覽器發送的預檢請求OPTIONS上設置必須的CORS響應頭信息即可允許跨域。
@WebFilter(filterName = "cors", urlPatterns = {"/*"})
public class CorsFilter implements Filter {
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 設置允許源,若出於安全,可以設置允許訪問的用戶源
response.setHeader("Access-Control-Allow-Origin", "*");
// 這裏通過判斷請求的方法,判斷此次是否是預檢請求,如果是,則進行跨域的設置
if ("OPTIONS".equals(request.getMethod())){
// 設置狀態碼爲204,允許跨域
response.setStatus(HttpStatus.SC_NO_CONTENT);
// 必選,表明服務器支持的所有跨域請求的方法。
response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS");
// 可選, 設定允許請求的頭部類型
response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, Token");
// 可選,預檢有效保持時間
response.addHeader("Access-Control-Max-Age", "1800");
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
4.2 CORS全局配置
這類似於使用過濾器,但可以在Spring MVC中聲明。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 是否發送Cookie
.allowCredentials(true)
// 放行哪些原始域
.allowedOrigins("*")
// 允許的請求類型
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
// 允許的請求頭
.allowedHeaders("*")
// 允許OPTION請求攜帶的字段,相當於Access-Control-Expose-Headers
.exposedHeaders("*");
}
}
4.3 CorsFilter(全局跨域)
在任意配置類,返回一個 新的 CorsFIlter Bean ,並添加映射路徑和具體的CORS配置路徑。
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
// 添加 CORS配置信息
CorsConfiguration config = new CorsConfiguration();
// 放行哪些原始域
config.addAllowedOrigin("*");
// 是否發送 Cookie
config.setAllowCredentials(true);
// 放行哪些請求方式
config.addAllowedMethod("*");
// 放行哪些原始請求頭部信息
config.addAllowedHeader("*");
// 暴露哪些頭部信息
config.addExposedHeader("*");
// 添加映射路徑
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**",config);
// 返回新的CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}
4.4 使用註解 (局部跨域)
@CrossOrigin:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
String[] DEFAULT_ORIGINS = { "*" };
String[] DEFAULT_ALLOWED_HEADERS = { "*" };
boolean DEFAULT_ALLOW_CREDENTIALS = true;
long DEFAULT_MAX_AGE = 1800;
/**
* 同origins屬性
*/
@AliasFor("origins")
String[] value() default {};
/**
* 所有支持域的集合,例如"http://zeng.com"。
* 這些值都顯示在請求頭中的Access-Control-Allow-Origin
* "*"代表所有域的請求都支持
* 默認支持所有請求域
* @see #value
*/
@AliasFor("value")
String[] origins() default {};
/**
* 允許請求頭重的header,默認都支持
*/
String[] allowedHeaders() default {};
/**
* 響應頭中允許訪問的header,默認爲空
*/
String[] exposedHeaders() default {};
/**
* 請求支持的方法,例如"{RequestMethod.GET, RequestMethod.POST}"}。
* 默認支持RequestMapping中設置的方法
*/
RequestMethod[] methods() default {};
/**
* 是否允許cookie隨請求發送,使用時必須指定具體的域
*/
String allowCredentials() default "";
/**
* 預請求的結果的有效期,默認30分鐘
*/
long maxAge() default -1;
}
在控制器上使用註解 @CrossOrigin:
@RestController
@CrossOrigin
public class HelloController {
@PostMapping("/hello")
public String hello() {
return "hello world";
}
}
複製代碼
在方法上使用註解 @CrossOrigin:
@RequestMapping("/hello")
@CrossOrigin(origins = "*")
public String hello() {
return "hello world";
}
4.5 手動設置響應頭(局部跨域)
使用 HttpServletResponse 對象添加響應頭(Access-Control-Allow-Origin)來授權原始域,這裏 Origin的值也可以設置爲 “*”,表示全部放行。該方法使用不方便,使用較少。
@RequestMapping("/index")
public String index(HttpServletResponse response) {
response.addHeader("Access-Allow-Control-Origin","*");
return "index";
}