所謂同源(即指在同一個域)就是兩個頁面具有相同的協議(protocol),主機(host)端口號(port)
同源策略是瀏覽器的一個安全功能,不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方資源。 同源策略是瀏覽器安全的基石
同源策略會阻止一個域的 javascript 腳本和另外一個域的內容進行交互。例如辦公內外網環境,當我們訪問外網一個惡意網站的時候,惡意網站就會利用我們的主機向內網的 url 發送 ajax 請求,破壞或盜取數據
瀏覽器的非同源限制以及3種解決思路
非同源限制
- 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB
- 無法接觸非同源網頁的 DOM
- 無法向非同源地址發送 AJAX 請求,即 XHR 請求
跨域的解決思路 1 —— 避免非同源限制
- 讓瀏覽器不做限制,指定參數,讓瀏覽器不做校驗,但該方法不太合理,因為它需要每個人都去做改動
- 不要發出 XHR 請求,這樣就算是跨域,瀏覽器也不會有非同源限制,解決方案是 JSONP,通過動態創建一個 script,通過 script 發出請求
跨域的解決思路 2 —— 跨源資源共享方案
- 根據 W3C 的跨源資源共享方案,在被調用方修改代碼,加上字段,告訴瀏覽器該網站支持跨域
跨域的解決思路 3 —— 隱藏跨域
- 使用 Nginx 反向代理,在 a 域名里面的的請求地址使用反向代理指向 b 域名,讓瀏覽器以為一直在訪問 a 網站,不觸發跨域限制
JSONP
- 普通請求值 XHR,希望得到服務端返回的 content-type 一般是 json
- JSONP 發出的是 script 請求,希望得到的返回是 js 腳本
Content-Type 是指 http/https 發送信息至服務端時的內容編碼類型,在 HTTP 協議消息頭中,使用 Content-Type 來表示請求和響應中的媒體類型信息。它用來告訴服務端如何處理請求的數據,以及告訴客戶端(一般是瀏覽器)如何解析響應的數據,比如顯示圖片,解析並展示 html 等等。
並不是請求或響應獨有的參數
JSONP 原理
以 JQuery 為例,發送 ajax 請求的時候,設置dataType:"jsonp"
,將使用 JSONP 方式調用函數,函數的 url 變為myurl?callback=e5bbttt
的形式,e5bbttt 就是一個臨時方法名,后端會根據callback
的值返回一個 js 腳本,如
<script>
e5bbttt({"a":"aaa","b":"bbb"});
</script>
JQuery 會提前根據 ajax 中 success 的內容生成一個臨時函數,名字就是 xxx
$.ajax({
// 其他省略
dataType:"jsonp",
success:function(data){
console.log(data.a);
console.log(data.b);
},
jsonp:"e5bbttt"
})
// JQuery 生成的臨時函數
function e5bbttt(data){
ajaxObject.success(data);
}
服務端返回給客戶端的e5bbttt({"a":"aaa","b":"bbb"});
,相當於調用立即(?)調用了 JQuery 生成的e5bbttt
函數,用完這個函數就銷毀了(?)
JSONP 也算是一個約定俗成的“協議”,callback 是約定俗成的作為定義臨時函數名的參數。如果想自定義這個參數名,需要在 ajax 中用 jsonp 屬性定義。
JSONP 的弊端
- 需要服務器改動代碼
- 只支持 GET 請求
- 發送的不是 xhr 請求
- 不安全
后端解決跨域
跟用戶數據有關的就是動態請求,沒有數據的是靜態請求,比如 css js,so,HTTP 服務器(Apache、Nginx 等)至少做了兩個作用
- HTTP 服務器,處理靜態請求
- 反向代理,負載均衡
在服務器端解決跨域有2種解決思路
- 在被調用后端應用解決:在響應頭增加指定字段,告訴瀏覽器允許調用。這種解決方案的請求是直接從瀏覽器發送給后端服務器,在瀏覽器上會看到 b.com 的 url
- 在前端服務器解決:這是隱藏跨域的解決方案。這種跨域請求不是直接從瀏覽器發送的,而是從中間的 http 服務器(前端應用所在服務器)轉發過去的,在瀏覽器中看到的還是 a.com 的 url,所以不會認為是跨域。但是該到 b.com 的請求還是會到 b.com
跨域原理及后端解決思路
依據瀏覽器同源策略,非同源腳本不可操作其他源下面的對象。想要操作其他源下的對象就需要跨域。綜上所述,在同源策略的限制下,非同源的網站之間不能發送 ajax 請求。如有需要,可通過降域或其他技術實現。
為了解決瀏覽器跨域問題,W3C 提出了跨源資源共享方案,即 CORS(Cross-Origin Resource Sharing)。
CORS 可以在不破壞即有規則的情況下,通過后端服務器實現 CORS 接口,就可以實現跨域通信。
CORS 將請求分為兩類:簡單請求和非簡單請求,分別對跨域通信提供了支持。
簡單請求
- 在 CORS 出現前,發送 HTTP 請求時在頭信息中不能包含任何自定義字段,且 HTTP 頭信息不超過以下幾個字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type 僅為這3種
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 請求方法是 GET HEAD POST 且滿足條件1
一個簡單請求:
GET /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, sdch, br
Origin: http://www.test.com
Host: www.test.com
對於簡單請求,CORS 的策略是請求時在請求頭中增加一個 Origin 字段,表示請求發出的域。服務器收到請求后,根據該字段判斷是否允許該請求訪問。
- 如果允許,則在 HTTP 頭信息中添加 Access-Control-Allow-Origin 字段,並返回正確的結果
- 如果不允許,則不添加 Access-Control-Allow-Origin 字段
除了上面提到的 Access-Control-Allow-Origin,還有幾個字段用於描述 CORS 返回結果
- Access-Control-Allow-Credentials:可選,用戶是否可以發送、處理cookie
- Access-Control-Expose-Headers:可選,可以讓用戶拿到的字段。有幾個字段無論是否允許跨域都可以拿到的:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
非簡單請求
一般是發送 JSON 格式的 ajax 請求,或帶有自定義頭的請求
對於非簡單請求的跨源請求,瀏覽器會在真實請求發出前,增加一次 OPTION 請求,稱為預檢請求(preflightrequest)。預檢請求將真實請求的信息,包括請求方法、自定義頭字段、源信息添加到 HTTP 頭信息字段中,詢問服務器是否允許這樣的操作
例如一個 GET 請求的預檢請求,包含一個自定義參數 X-Custom-Header
OPTIONS /test HTTP/1.1
Origin: http://www.test.com
Access-Control-Request-Method: GET // 請求使用的 HTTP 方法
Access-Control-Request-Headers: X-Custom-Header // 請求中包含的自定義頭字段
Host: www.test.com
服務器收到請求時,需要分別對 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 進行驗證,驗證通過后,會在返回 HTTP 頭信息中添加:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.test.com // 允許的域
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允許的方法
Access-Control-Allow-Headers: X-Custom-Header // 允許的自定義字段
Access-Control-Allow-Credentials: true // 是否允許用戶發送、處理 cookie
Access-Control-Max-Age: 172800 // 預檢請求的有效期,單位為秒。有效期內,不需要發送預檢請求,ps 48小時
當預檢請求通過后,瀏覽器才會發送真實請求到服務器。這樣就實現了跨域資源的請求訪問。
所以后端處理其實處理的就是這次預檢請求
注意:
在 Chrome 和 Firefox 中,如果 Access-Control-Allow-Methods 中並未允許 GET/POST/HEAD 請求,但允許跨域了,瀏覽器還是會允許 GET/POST/HEAD 這些簡單請求訪問,這時就必須在后台用其他辦法禁掉這些 Method
后端應用處理 - Filter&HttpServletResponse 方法
這種方法不會用到 Spring,對 Servlet 也可以使用
在 web.xml 中配置
<!-- 跨域 -->
<filter>
<filter-name>webFliter</filter-name>
<filter-class>com.n031.filter.WebFliter</filter-class>
</filter>
<filter-mapping>
<filter-name>webFliter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
編寫 java 類
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class WebFliter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse) response;
// 允許跨域的域名,設置*表示允許所有域名
String origin = req.getHeader("Origin");
if ("abcdefg".contains(origin)) { // 滿足指定的條件
res.addHeader("Access-Control-Allow-Origin", origin);
}
res.addHeader("Access-Control-Allow-Origin", "http://www.test.com");
// 允許跨域的方法,可設置*表示所有
res.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
// 允許的自定義字段
String headers = req.getHeader("Access-Control-Request-Headers"); // 獲取 request 發來的自定義字段
res.addHeader("Access-Control-Allow-Headers", headers);
// 或者
// res.addHeader("Access-Control-Allow-Headers", "X-Custom-Header");
// 預檢請求的有效期,單位為秒。有效期內,不需要發送預檢請求,ps 48小時
res.addHeader("Access-Control-Max-Age", "172800");
// 還可以有其他配置...
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
后端應用處理 - Spring 方法
Spring 解決跨域的方法很多,感覺就和茴字有五種寫法一樣。這里列舉的並不全。
先看下原理。說實話雖然搞不懂為什么這么做,但看了下這個類的源碼確實是這么寫的。
本質都是構造CorsConfiguration
然后委托給DefaultCorsProcessor
實現(責任鏈模式,要學的東西好多啊...)
public class CorsConfiguration {
private List<String> allowedOrigins;
private List<String> allowedMethods;
private List<String> allowedHeaders;
private List<String> exposedHeaders;
private Boolean allowCredentials;
private Long maxAge;
}
DefaultCorsProcessor
的processRequest
處理步驟如下(spring-web 5.1.8-RELEASE
)
- 判斷是否是包含 Origin 字段,不包含就放行,否則繼續判斷
- 判斷 Response 的 Header 是否已經包含 Access-Control-Allow-Origin。如果包含,證明已經被處理過了,放行,否則繼續判斷
- 判斷是否同源,如果是則放行,否則繼續判斷
- 到此步基本已經得出這是個跨域請求的結論。然后看配置了 CORS 規則
- 沒有配置,且是預檢請求,則拒絕該請求(說明該應用禁止跨域)
- 沒有配置,且不是預檢請求,跳過跨域處理(有可能導致返回數據被瀏覽器攔截)
- 配置了,則根據配置的規則(
CorsConfiguration
)決定是否放行
在 Controller 上添加 @CrossOrigin 注解
這種方式適合只有一兩個 rest 接口需要跨域或者沒有網關的情況下,這種處理方式就非常簡單,適合在原來基代碼基礎上修改,影響比較小。
@CrossOrigin(allowCredentials = "true", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE}, origins = "*")
@PostMapping("/abc")
public String handler(@RequestBody String json) {
return "abc";
}
增加 WebMvcConfigurer 全局配置
@Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")// 允許跨域的訪問路徑
.allowedOrigins("*")// 允許跨域訪問的源
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")// 允許請求方法
.maxAge(172800)// 預檢間隔時間
.allowCredentials(true);// 是否允許發送 cookie
}
}
注意由於 Java8 開始支持 default method,這個類從 spring 5.0 開始已經過期,未來這個方法將轉移到WebMvcConfigurer
接口中
default void addCorsMappings(CorsRegistry registry){}
結合 Filter 使用
其實和方法2類似,都是構造CorsConfiguration
@Configuration
public class CorsConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 是否發送cookie
config.setAllowCredentials(true);
// 允許的網站域名,全允許則設為 *
config.addAllowedOrigin("http://localhost:8088");
// 允許 HEADER 或 METHOD , * 為全部
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
// 這個順序很重要,為避免麻煩請設置在最前
bean.setOrder(0);
return bean;
}
}
以上這種方案如果微服務多的話,需要在每個服務的主類上都加上這么段代碼,增加了維護量。
這三種方案都是在 SpringBoot 的基礎上實現的解決方案,在模塊較多或者接口較多的情況下不易維護。
既然 Spring Cloud 自帶 Gateway,下面就講講使用 Gateway 的跨域解決方案。(Gateway 是取代不斷跳票的 Zuul 的新一代網關)
在 Gateway 增加 CorsFilter 攔截器
4 5 方法未驗證
這種方案跟方案三有些類似,只不過是放到了 Gateway 端,對於有多個微服務模塊的情況下,就大大減少了 SpringBoot 模塊端的代碼量,讓各個模塊更集中精力做業務邏輯實現。這個方案只需要在 Gateway 里添加 Filter 代碼類即可。
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import javax.servlet.http.HttpServletRequest;
@Configuration
public class CorsWebFilter implements WebFilter {
private static final String ALL = "*";
private static final String MAX_AGE = "18000";
@Override
public Mono<Void> filter(ServerWebExchange ctx, WebFilterChain chain) {
ServerHttpRequest request = ctx.getRequest();
String path = request.getPath().value();
ServerHttpResponse response = ctx.getResponse();
if ("/favicon.ico".equals(path)) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
if (!CorsUtils.isCorsRequest((HttpServletRequest) request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, ALL);
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
}
}
修改 Gateway 配置文件
在仔細閱讀過 Gateway 的文檔你就會發現,原來 CorsFilter 早已經在 Gateway 里了,不需要自己寫代碼實現,而且更靈活,修改配置文件即可,結合配置中心使用,可以實現動態修改。
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "docs.spring.io"
allowedMethods:
- GET
后端服務器處理 - Ngnix 方法
這里的 Nginx 盡做反向代理功能,瀏覽器訪問頁面在 a.com 的 Nginx 上,ajax 請求接口是 b.com,所以瀏覽器認為是跨域
Nginx 在 nginx.conf 上配(vhost 是約定做法,這樣做不修改主文件)
include vhost/*.config;
創建 cors.conf
server{
listen 80; // 監聽80端口
server_name b.com; // 監聽向 b.com 發送的請求
location /{
proxy_pass http://ser432ver.53253bb.com:8080; // 轉發到哪里
// Filter實現的功能在Nginx上再實現一遍
add_header Access-Control-Allow-Origin $http_origin; // $http_ 可以獲取請求中相應的 header 參數
add_header Access-Control-Allow-Method *;
add_header Access-Control-Allow-Headers X-Custom-Header;
// 或者
// add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Allow-Credentials true;
add_header Access-Max-age 172800;
// 直接處理預檢命令,if 后要帶空格
if ($request_method = OPTIONS) {
return 200;
}
}
}
前端服務器解決跨域
但其實大部分情況下,我們會把前端應用和請求轉發放在同一台 Nginx 上
server{
listen 80; // 監聽80端口
server_name a.com; // 監聽向 a.com 發送的請求
location / {
root html;
index index.html index.htm;
}
locltion /ajaxserver {
proxy_pass http://ser432ver.53253bb.com:8080; // 后端地址
}
}
這樣實質是隱藏跨域,讓瀏覽器認為沒有訪問其他域就不會發生跨域。
前端代碼需要在每個 ajax 請求前都要加上/ajaxserver
參考資料
ajax跨域完全講解
https://www.imooc.com/learn/947
SpringBoot使用CORS解決跨域請求問題
https://www.cnblogs.com/7788IT/p/10693073.html
Spring MVC之@RequestParam @RequestBody @RequestHeader 等詳解
https://blog.csdn.net/summerSunStart/article/details/78676781
你不知道的「跨域 CORS」
https://www.jianshu.com/p/abb5f6bf92c3
關於跨域問題和安全性的一點理解
https://blog.csdn.net/jaytalent/article/details/52213576
淺談跨域威脅與安全
https://www.freebuf.com/articles/web/208672.html
cors跨域中關於access-control-allow-headers導致的錯誤
https://www.jianshu.com/p/cecb73b26a11
什么是跨域?跨域解決方法
https://blog.csdn.net/qq_38128179/article/details/84956552
Spring Cloud配置跨域訪問的五種方案?你用的是哪一種呢?
https://segmentfault.com/a/1190000017188296
servlet跨域請求
https://blog.csdn.net/qq_34135615/article/details/82900786
跨域(CORS) 解決方案中,為什么 Access-Control-Allow-Methods 不起作用?
https://segmentfault.com/q/1010000005067552/a-1020000005067822