天天看點

跨域問題整理

什麼是跨域請求

當一台伺服器資源從另一台伺服器(不同的域名或者端口)請求一個資源或者接口,就會發起一個跨域 HTTP 請求。比如從http://aaa.com/index.html,發送一個 Ajax 請求,請求位址是 http://bbb.com/下面的一個接口,這就是發起了一個跨域請求。在不做任何處理的情況下,這個跨域請求是無法被成功請求的,因為浏覽器基于同源政策會對跨域請求做一定的限制。

什麼是同源政策

如果不是浏覽器的話, 就不會受到同源政策的影響。也就是說,兩個伺服器直接進行跨域請求是可以進行資料請求的。

同源政策的目的是限制從同一個源加載的文檔或者腳本如何與來自另 一個源的資源進行互動。這是一個用于隔離潛在惡意檔案的重要安全機制。

限制内容包括:Cookie、LocalStorage、IndexedDB 等存儲性内容、DOM 節點、AJAX 請求不能發送。

當協定、域名、端口号中任意一個不相同時 , 都算作不同源(必須是域名完全相同,比如說 a.example.com 和b.example.com 這兩個域名。雖然它們的頂級域名和二級域名(均為 example.com)都相同,但是三級域名(a 和 b)不相同,是以也不能算作域名相同)。如果不同時滿足這上面三個條件,那就不符合浏覽器的同源政策。

注意的是,不是所有的互動都會被同源政策攔截下來,下面兩種互動就不會觸發同源政策,舉例如下:

  • 跨域寫操作(Cross-origin writes):

    例如超連結、重定向以及表單的送出操 作,特定少數的 HTTP

    請求需要添加預檢請求(preflight)

  • 跨域資源嵌入(Cross-origin embedding):

跨域解決方案

  1. CORS(Cross-origin resource sharing),跨域資源共享CORS 其實是浏覽器制定的一個規範,浏覽器會自動進行 CORS 通信,它的實作則主要在服務端,它通過一些 HTTP Header 來限制可以通路的域,例如頁面 A 需要通路 B 伺服器上的資料,如果 B 伺服器 上聲明了允許 A 的域名通路,那麼從 A 到 B 的跨域請求就可以完成。我們隻需要設定響應頭,即可進行跨域請求。

    浏覽器将CORS請求分成兩類:簡單請求和非簡單請求。對于兩種請求有不同的處理方式。

1.簡單請求

隻要同時滿足以下兩大條件,就屬于簡單請求:

(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

對于簡單請求,浏覽器直接發出CORS請求。具體來說,就是在頭資訊之中,增加一個Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
           

如果本次跨域請求,不在許可範圍内,伺服器會傳回一個正常的HTTP回應。浏覽器發現,這個回應的頭資訊沒有包含Access-Control-Allow-Origin字段,就知道出錯了,進而抛出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤無法通過狀态碼識别,因為HTTP回應的狀态碼有可能是200。

如果本次跨域請求在許可範圍内,伺服器傳回的響應,會多出幾個頭資訊字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
           

對以上三個與CORS請求相關且都以Access-Control-開頭的字段作出解釋。

  • Access-Control-Allow-Origin

該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求。

  • Access-Control-Allow-Credentials

該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。預設情況下,Cookie不包括在CORS請求之中。設為true,即表示伺服器明确許可,Cookie可以包含在請求中,一起發給伺服器。這個值也隻能設為true,如果伺服器不要浏覽器發送Cookie,删除該字段即可。

  • 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字段的值。

**另外,CORS請求預設不發送Cookie和HTTP認證資訊。**如果要把Cookie發到伺服器,一方面要伺服器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true
           

另一方面,我們必須在AJAX請求中打開withCredentials屬性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
           

否則,即使伺服器同意發送Cookie,浏覽器也不會發送。或者,伺服器要求設定Cookie,浏覽器也不會處理。

但是,如果省略withCredentials設定,有的浏覽器還是會一起發送Cookie。這時,可以顯式關閉withCredentials。

xhr.withCredentials = false;
           

**需要注意的是,如果要發送Cookie,Access-Control-Allow-Origin就不能設為星号,必須指定明确的、與請求網頁一緻的域名。**同時,Cookie依然遵循同源政策,隻有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie并不會上傳,且(跨源)原網頁代碼中的document.cookie也無法讀取伺服器域名下的Cookie。

2.非簡單請求

非簡單請求是那種對伺服器有特殊要求的請求,比如請求方式是 PUT、DELETE,或者 Content-Type 字段類型是 application/json,HTTP頭資訊中加了自定義的header。非簡單請求的 CORS請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為預檢請求(preflight),預檢請求用的請求方法是 OPTIONS。

浏覽器先詢問伺服器,目前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些HTTP動詞和頭資訊字段。隻有得到肯定答複,浏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。

下面是一段浏覽器的JavaScript腳本。

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頭資訊。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
           

"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭資訊裡面,關鍵字段是Origin,表示請求來自哪個源。

除了Origin字段,"預檢"請求的頭資訊包括兩個特殊字段。

Access-Control-Request-Method:該字段是必須的,用來列出浏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。

Access-Control-Request-Headers:該字段是一個逗号分隔的字元串,指定浏覽器CORS請求會額外發送的頭資訊字段,上例是X-Custom-Header。

伺服器收到"預檢"請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以後,确認允許跨源請求,就可以做出回應。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
           

如果浏覽器否定了"預檢"請求,會傳回一個正常的HTTP回應,但是沒有任何CORS相關的頭資訊字段。這時,浏覽器就會認定,伺服器不同意預檢請求,是以觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。控制台會列印出如下的報錯資訊。

XMLHttpRequest cannot load http://api.alice.com.

Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

一旦伺服器通過了"預檢"請求,以後每次浏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭資訊字段。伺服器的回應,也都會有一個Access-Control-Allow-Origin頭資訊字段。

下面是"預檢"請求之後,浏覽器的正常CORS請求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
           

上面頭資訊的Origin字段是浏覽器自動添加的。

下面是伺服器正常的回應。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
           

一般我們可以寫一個過濾器或攔截器(這裡以過濾器為例):

@WebFilter(filterName = "corsFilter", urlPatterns = "/*",
        initParams = {@WebInitParam(name = "allowOrigin", value = "*"),
                @WebInitParam(name = "allowMethods", value = "GET,POST,PUT,DELETE,OPTIONS"),
                @WebInitParam(name = "allowCredentials", value = "true"),
                @WebInitParam(name = "allowHeaders", value = "Content-Type,X-Token")})
public class CorsFilter implements Filter {
 
    private String allowOrigin;
    private String allowMethods;
    private String allowCredentials;
    private String allowHeaders;
    private String exposeHeaders;
 
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        allowOrigin = filterConfig.getInitParameter("allowOrigin");
        allowMethods = filterConfig.getInitParameter("allowMethods");
        allowCredentials = filterConfig.getInitParameter("allowCredentials");
        allowHeaders = filterConfig.getInitParameter("allowHeaders");
        exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (!StringUtils.isEmpty(allowOrigin)) {
            if(allowOrigin.equals("*")){
                // 設定哪個源可以通路
                response.setHeader("Access-Control-Allow-Origin", allowOrigin);
            }else{
                List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));
                if (allowOriginList != null && allowOriginList.size() > 0) {
                    String currentOrigin = request.getHeader("Origin");
                    if (allowOriginList.contains(currentOrigin)) {
                        response.setHeader("Access-Control-Allow-Origin", currentOrigin);
                    }
                }
            }
        }
        if (!StringUtils.isEmpty(allowMethods)) {
            //設定哪個方法可以通路
            response.setHeader("Access-Control-Allow-Methods", allowMethods);
        }
        if (!StringUtils.isEmpty(allowCredentials)) {
            // 允許攜帶cookie
            response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
        }
        if (!StringUtils.isEmpty(allowHeaders)) {
            // 允許攜帶哪個頭
            response.setHeader("Access-Control-Allow-Headers", allowHeaders);
        }
        if (!StringUtils.isEmpty(exposeHeaders)) {
            // 允許攜帶哪個頭
            response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
 
    @Override
    public void destroy() {
 
    }
    }
           
CrossOrigin注解

這個方法僅對Java有用。springboot中,在Controller類上添加一個 @CrossOrigin(origins ="*") 注解就可以實作對目前controller 的跨域通路了,當然這個标簽也可以加到方法上,或者直接加到入口類上對所有接口進行跨域處理,注意這個注解隻在JDK1.8版本以上才起作用。

@CrossOrigin下的很多的屬性

@AliasFor("origins")
String[] value() default {};
 
@AliasFor("value")
String[] origins() default {};
 
String[] allowedHeaders() default {};
 
String[] exposedHeaders() default {};
 
RequestMethod[] methods() default {};
 
String allowCredentials() default "";
 
long maxAge() default -1L;
           
  • String[] origins: 允許來源域名的清單,例如 ‘www.jd.com’,比對的域名是跨域預請求 Response 頭中的

    ‘Access-Control-Aloow_origin’ 字段值。不設定确切值時預設支援所有域名跨域通路。

  • String[] allowedHeaders: 跨域請求中允許的請求頭中的字段類型, 該值對應跨域預請求 Response 頭中的 ‘Access-Control-Allow-Headers’ 字段值。

    不設定确切值預設支援所有的header字段(Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma)跨域通路。

  • String[] exposedHeaders:跨域請求請求頭中允許攜帶的除Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma這六個基本字段之外的其他字段資訊,對應的是跨域請求Response 頭中的 'Access-control-Expose-Headers’字段值。
  • RequestMethod[] methods: 跨域HTTP請求中支援的HTTP請求類型(GET、POST…),不指定确切值時預設與Controller 方法中的 methods 字段保持一緻。
  • String allowCredentials: 該值對應的是是跨域請求 Response 頭中的 ‘Access-Control-Allow-Credentials’ 字段值。浏覽器是否将本域名下的 cookie資訊攜帶至跨域伺服器中。預設攜帶至跨域伺服器中,但要實作 cookie 共享還需要前端在 AJAX 請求中打開withCredentials 屬性。
  • long maxAge: 該值對應的是是跨域請求 Response 頭中的 'Access-Control-Max-Age’字段值,表示預檢請求響應的緩存持續的最大時間,目的是減少浏覽器預檢請求/響應互動的數量。預設值1800s。設定了該值後,浏覽器将在設定值的時間段内對該跨域請求不再發起預請求。

    這裡我們重點關注RequestMethod[] methods() default {};RequestMethod屬性如果不設定的話,預設與 Controller 方法中的 methods 字段保持一緻。在我們的項目中就是使用的預設值,而我們的請求時GET請求,是以對于跨域HTTP請求隻會支援GET請求,對于預檢請求OPTION請求當然是拒絕啦;和String[] exposedHeaders() default {};這裡我們也沒有設定值,是以我們的新加的請求頭是無法通過的。

    代碼示例如下:

@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
           
4.使用SpringCloud網關(未遇到過,待詳細補充)

服務網關(zuul)又稱路由中心,用來統一通路所有api接口,維護服務。

Spring Cloud Zuul通過與Spring Cloud Eureka的整合,實作了對服務執行個體的自動化維護,是以在使用服務路由配置的時候,我們不需要向傳統路由配置方式那樣去指定具體的服務執行個體位址,隻需要通過Ant模式配置檔案參數即可

5.Node中間件代理(兩次跨域)(未遇到過,待詳細補充)

實作原理:同源政策是浏覽器需要遵循的标準,而如果是伺服器向伺服器請求就無需遵循同源政策。這樣的話,我們可以讓伺服器替我們發送一個請求,請求其他伺服器下面的資料。然後我們的頁面通路目前伺服器下的接口就沒有跨域問題了。 代理伺服器,需要做以下幾個步驟:

跨域問題整理

接受用戶端請求 。

将請求 轉發給伺服器。

拿到伺服器 響應 資料。

将 響應 轉發給用戶端。

6.nginx反向代理(未遇到過,待詳細補充)

實作原理類似于Node中間件代理,需要你搭建一個中轉nginx伺服器,用于轉發請求。

使用nginx反向代理實作跨域,是最簡單的跨域方式。隻需要修改nginx的配置即可解決跨域問題,支援所有浏覽器,支援session,不需要修改任何代碼,并且不會影響伺服器性能。

實作思路:通過nginx配置一個代理伺服器做跳闆機,反向代理通路domain2接口,并且可以順便修改cookie中domain資訊,友善目前域cookie寫入,實作跨域登入。

将nginx目錄下的nginx.conf修改如下:

// proxy伺服器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie裡域名
        index  index.html index.htm;
 
        # 當用webpack-dev-server等中間件代理接口通路nignx時,此時無浏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #目前端隻跨域不帶cookie時,可為*
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Allow-Methods GET, POST, OPTIONS;
        add_header Access-Control-Allow-Headers *;
    }
}
           

這樣我們的前端代理隻要通路 http:www.domain1.com:81/*就可以了。

繼續閱讀