一文弄懂 CORS 跨域(前端+後端代碼實例講解)

常常被問到一些問題,好比寫 Java 服務端的同窗的來問:我服務端明明正確返回了,測試環境 debug 能看到,爲何前端就是拿不到數據? 而後寫前端的同窗會問:爲何我明明設置了 withCredentials=true,服務端同窗仍是拿不到 cookie?javascript

因此決定從新捋一捋使用 CORS 解決跨域的問題,先後端要怎麼作?爲何這麼作?html

爲何會有跨域的問題?

爲了保證用戶信息的安全,全部的瀏覽器都遵循同源策略,那什麼狀況下算同源呢?同源策略又是什麼呢?前端

記住:協議、域名、端口號徹底相同時,纔是同源java

能夠參考 Web安全 - 瀏覽器的同源策略git

在同源策略下,會有如下限制:github

  • 沒法獲取非同源的 Cookie、LocalStorage、SessionStorage 等
  • 沒法獲取非同源的 dom
  • 沒法向非同源的服務器發送 ajax 請求

可是咱們又常常會遇到先後端分離,不在同一個域名下,須要ajax請求數據的狀況。那咱們就要規避這種限制。web

能夠在網上搜到不少解決跨域的方法,有些方法比較古老了,如今項目中用的比較多的是 jsonp 和 CORS(跨域資源共享),這篇主要講 CORS 的原理和具體實踐。ajax

CORS 跨域原理

CORS 跨域的原理其實是瀏覽器與服務器經過一些 HTTP 協議頭來作一些約定和限制。能夠查看 HTTP-訪問控制(CORS)數據庫

與跨域相關的協議頭json

請求頭 說明
Origin 代表預檢請求或實際請求的源站 URI,不論是否跨域ORIGIN 字段老是被髮送
Access-Control-Request-Method 將實際請求所使用的 HTTP 方法告訴服務器
Access-Control-Request-Headers 將實際請求所攜帶的首部字段告訴服務器
響應頭 說明
Access-Control-Allow-Origin 指定容許訪問該資源的外域 URI,對於攜帶身份憑證的請求不可以使用通配符*
Access-Control-Expose-Headers 指定 XMLHttpRequest的getResponseHeader 能夠訪問的響應頭
Access-Control-Max-Age 指定 preflight 請求的結果可以被緩存多久
Access-Control-Allow-Credentials 是否容許瀏覽器讀取 response 的內容;
當用在 preflight 預檢請求的響應中時,指定實際的請求是否可以使用 credentials
Access-Control-Allow-Methods 指明實際請求所容許使用的 HTTP 方法
Access-Control-Allow-Headers 指明實際請求中容許攜帶的首部字段

代碼實例

這裏寫了個 demo,一步步來分析。目錄以下:

.
├── README.md
├── client
│   ├── index.html
│   └── request.js
└── server
    ├── pom.xml
    ├── server-web
    │   ├── pom.xml
    │   ├── server-web.iml
    │   └── src
    │       └── main
    │           ├── java
    │           │   └── com
    │           │       └── example
    │           │           └── cors
    │           │               ├── constant
    │           │               │   └── Constants.java
    │           │               ├── controller
    │           │               │   └── CorsController.java
    │           │               └── filter
    │           │                   └── CrossDomainFilter.java
    │           ├── resources
    │           │   └── config
    │           │       └── applicationContext-core.xml
    │           └── webapp
    │               ├── WEB-INF
    │               │   ├── dispatcher-servlet.xml
    │               │   └── web.xml
    │               └── index.jsp
    └── server.iml
複製代碼

  • Client:前端,簡單的ajax請求

在client文件夾下,啓動靜態服務器,前端頁面經過http://localhost:8000/index.html訪問:

anywhere -h localhost -p 8000
複製代碼

  • Server: java項目,SpringMVC

在 IntelliJ IDEA 中本地啓動 tomcat,設置host: http://localhost:8080/,服務端數據經過http://localhost:8080/server/cors請求。

這裏前端和後端由於端口號不一樣,存在跨域限制,下面經過 CORS 來解決由於跨域沒法經過ajax請求數據的問題。


沒有容許跨域的狀況

這種狀況就是前端什麼都不作,服務端也什麼都不作。

Client: 請求成功後,將數據顯示在頁面上

new Request().send('http://localhost:8080/server/cors',{
	success: function(data){
		document.write(data)
	}
});
複製代碼

Server:

@Controller
@RequestMapping("/server")
public class CorsController {
    @RequestMapping(value="/cors", method= RequestMethod.GET)
    @ResponseBody
    public String ajaxCors(HttpServletRequest request) throws Exception{
        return "SUCCESS";
    }
}
複製代碼

在瀏覽器地址欄輸入http://localhost:8080/server/cors直接請求服務端,能夠看到返回結果: ‘SUCCESS’

server_1.png

在瀏覽器地址欄輸入http://localhost:8000/index.html,從不一樣域的網頁中向 Server 發送 ajax 請求。能夠看到幾個方面:

從 network 能夠看到,請求返回正常。

client_1_network.png

但 Response 中沒有內容,顯示 Failed to load response data

client_network_response.png

而且控制檯報錯:

client_1_console_error.png

總結:

一、瀏覽器請求是發出去了的,服務端也會正確返回,可是咱們拿不到response的內容

二、瀏覽器控制檯會報錯提示能夠怎麼作,並且提示的很明白: xhr不能請求http://localhost:8080/server/cors,請求資源的響應頭中沒有設置Access-Control-Allow-Origin,Origin:http://localhost:8000是不容許跨域請求的。

那下一步,咱們要在服務端響應跨域請求時,設置響應頭: Access-Control-Allow-Origin

設置 Access-Control-Allow-Origin 容許跨域

先說明爲何要設置 Access-Control-Allow-Origin,能夠把 Access-Control-Allow-Origin 看成一個指令,服務端設置 Access-Control-Allow-Origin 就是告訴瀏覽器容許向服務端請求資源的域名,瀏覽器經過 Response 中的 Access-Control-Allow-Origin 就能夠知道能不能把數據吐出來。

官方解釋是這樣的: Access-Control-Allow-Origin 響應頭指定了該響應的資源是否被容許與給定的 origin 共享。

Access-Control-Allow-Origin能夠設置的值有:

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin:
複製代碼

那在java服務端給響應頭設置 Access-Control-Allow-Origin 能夠這麼作:

一、添加一個過濾器

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    public void destroy() {}
}
複製代碼

二、而後在web.xml文件中添加過濾器配置:

crossDomainFilter
    com.example.cors.filter.CrossDomainFilter
    crossDomainFilter
    /*
複製代碼

三、而後從新啓動tomcat,client從新發送請求http://localhost:8000/index.html

client_2_access_success.png

client_2_access_response.png

能夠看到咱們可以拿到返回結果了,響應頭中有咱們在服務端設置的Access-Control-Allow-Origin: http://localhost:8000,這個應該跟請求頭中的origin一致,或者設置Access-Control-Allow-Origin:*也是能夠的,這就容許任何網站來訪問資源了(前提是不帶憑證信息,這個後面講)

以上就是容許一個簡單的跨域請求的作法,只須要服務端設置響應頭Access-Control-Allow-Origin。

簡單請求與預檢請求

上面講述了一個簡單請求經過在服務端設置響應頭 Access-Control-Allow-Origin 就能夠完成跨域請求。

那怎樣的請求算是一個簡單請求?與簡單請求相對應的是什麼樣的請求呢?解決跨域的方式又有什麼不同呢?

符合如下條件的可視爲簡單請求:

一、使用下列 HTTP 方法之一

- GET
- HEAD
- POST,而且Content-Type的值在下列之一:
  - text/plain
  - multipart/form-data
  - application/x-www-form-urlencoded
複製代碼

二、而且請求頭中只有下面這些

- Accept
- Accept-Language
- Content-Language
- Content-Type (須要注意額外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
複製代碼

不知足上述要求的在發送正式請求前都要先發送一個預檢請求,預檢請求以 OPTIONS 方法發送,瀏覽器經過請求方法和請求頭可以判斷是否發送預檢請求。

好比 Client 發送以下請求:

new Request().send('http://localhost:8080/server/options',{
	method: 'POST',
	header: {
		'Content-Type': 'application/json'  //告訴服務器實際發送的數據類型
	},
	success: function(data){
		document.write(data)
	}
});
複製代碼

Server 端處理請求的 controller:

@Controller
@RequestMapping("/server")
public class CorsController {
    @RequestMapping(value="/options", method= RequestMethod.POST)
    @ResponseBody
    public String options(HttpServletRequest request) throws Exception{
        return "SUCCESS";
    }
}
複製代碼

由於請求時,請求頭中塞入了 header,'Content-Type': 'application/json'。根據前面講述的能夠知道,瀏覽器會以 OPTIONS 方法發出一個預檢請求,瀏覽器會在請求頭中加入:

Access-Control-Request-Headers:content-type
Access-Control-Request-Method:POST
複製代碼

這個預檢請求的做用在這裏就是告訴服務器:我會在後面請求的請求頭中以 POST 方法發送數據類型是application/json 的請求,詢問服務器是否容許。

client_3_options_error.png

在這裏服務器尚未作任何容許這種請求的設置,因此瀏覽器控制檯報錯:

client_3_options_console_error.png

也清楚的說明了出錯的緣由: 服務端在預檢請求的響應中沒有告訴瀏覽器容許協議頭 Content-Type,即服務端須要設置響應頭 Access-Control-Allow-Headers,容許瀏覽器發送帶 Content-Type 的請求。

Server端過濾器中添加Access-Control-Allow-Headers:

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    public void destroy() {}
}
複製代碼

能夠看到請求成功

client_3_options_console_success.png

再來看請求的具體信息,第一次以 OPTIONS 方法發送預檢請求,瀏覽器設置請求頭:

Access-Control-Request-Headers:content-type //請求中加入的請求頭
Access-Control-Request-Method:POST  //跨域請求的方法
複製代碼

服務端設置響應頭:

Access-Control-Allow-Headers:Content-Type   //容許的header
Access-Control-Allow-Origin:http://localhost:8000 //容許跨域的源
複製代碼

client_3_options_success.png

也能夠設置 Access-Control-Allow-Methods 來限制客戶端的的請求方法。

這樣預檢請求成功了,瀏覽器會發出第二個請求,這是真正請求數據的請求:

client_3_options_2_post.png

能夠看到 POST 請求成功了,第二次請求頭中沒有設置 Access-Control-Request-Headers 和 Access-Control-Request-Method。

可是這裏有個問題,須要預檢請求時,瀏覽器會發出兩次請求,一次 OPTIONS,一次 POST。兩次都返回了數據。這樣服務端若是邏輯複雜一些,好比去數據庫查找數據,從 web 層、 service 到數據庫這段邏輯就會走兩遍,瀏覽器會兩次拿到相同的數據,因此服務端的 filter 能夠改一下,若是是 OPTIONS 請求,在設置完跨域請求響應頭後就不走後面的邏輯直接返回。

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type");   
        //OPTION請求就直接返回
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getMethod().equals("OPTIONS")) {
            resp.setStatus(200);
            resp.flushBuffer();
        }else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
    public void destroy() {}
}
複製代碼

總結:

一、 對於 POST 請求設置響應頭Content-Type爲某些值、自定義請求頭等狀況,瀏覽器會先以OPTIONS方法發送一個預檢請求,並設置相應的請求頭。

二、 服務端仍是正常返回,但若是預檢請求響應頭中不設置相應的響應頭,預檢請求不經過,不會再發出第二次請求來獲取數據。

三、 服務端設置相應的響應頭,瀏覽器會發出第二個請求,並將服務端返回的數據吐出,咱們能夠得到response的內容

帶憑證信息的請求

還有一種狀況咱們常常遇到。瀏覽器在發送請求時須要給服務端發送 cookie,服務端根據 cookie 中的信息作一些身份驗證等。

默認狀況下,瀏覽器向不一樣域的發送 ajax 請求,不會攜帶發送 cookie 信息。

Client:

var containerElem = document.getElementById('container')
new Request().send('http://localhost:8080/server/testCookie',{
	success: function(data){
		containerElem.innerHTML = data
	}
});
複製代碼

Server:

@RequestMapping(value="/testCookie", method= RequestMethod.GET)
@ResponseBody
public String testCookie(HttpServletRequest request,HttpServletResponse response) throws Exception{
    String str = "SUCCESS";
    Cookie[] cookies = request.getCookies();
    String school = getSchool(cookies);
    if(school == null || school.length() == 0){
        addCookie(response);
    }
    return str + buildText(cookies);
}
複製代碼

服務端收到請求,判斷 cookie 中有沒有 school,沒有就添加 cookie.

client_4_cookie_none.png

能夠看到響應頭中有 Set-Cookie,再次請求時,若是是同源請求,瀏覽器會將 Set-Cookie 中的值放在請求頭中,可是對於跨域請求,默認是不發送這個 Cookie 的。

若是要讓瀏覽器發送 cookie,須要在 Client 設置 XMLHttpRequest 的 withCredentials 屬性爲 true。

Client:

var containerElem = document.getElementById('container')
new Request().send('http://localhost:8080/server/testCookie',{
	withCredentials: true,
	success: function(data){
		containerElem.innerHTML = data
	}
});
複製代碼

如今瀏覽器在請求頭中加入了 cookie 信息

client_4_cookie_error.png

可是服務端返回的數據沒有在頁面中展現,而且報錯:

client_4_credentials_error.png

報錯信息很明白: 當請求中包含憑證信息時,須要設置響應頭 Access-Control-Allow-Credentials,是否帶憑證信息是由 XMLHttpRequest的withCredentials 屬性控制的。
**
因此咱們在 Server 端 filter 中加入這個響應頭:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Credentials","true");
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getMethod().equals("OPTIONS")) {
            resp.setStatus(200);
            resp.flushBuffer();
        }else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
複製代碼

如今瀏覽器知道響應頭中 Access-Control-Allow-Credentials 爲 true,就會把數據給吐出來了,咱們可以從response 中拿到內容了。

client_4_cookie_success_console.png

client_4_cookie_success.png

那若是附帶憑證信息而且有預檢請求呢?若是有預檢請求,並附帶憑證信息( XMLHttpRequest 的withCredentials 設置爲 true), 服務端須要設置 Access-Control-Allow-Credentials: true,不然瀏覽器不會發出第二次請求,並報錯。

client_4_cookie_preflight_error.png

總結:

一、跨域請求時,瀏覽器默認不會發送cookie,須要設置XMLHttpRequest的withCredentials屬性爲true

二、 瀏覽器設置XMLHttpRequest的withCredentials屬性爲true,代表要向服務端發送憑證信息(這裏是cookie)。那麼服務端就須要在響應頭中添加Access-Control-Allow-Credentials爲true。不然瀏覽器上有兩種狀況:

  • 若是是簡單請求,服務端結果吐出了,瀏覽器拿到了但就是不給吐出來,並報錯。
  • 若是是預檢請求,一樣咱們拿不到返回結果,並報錯提示預檢請求不經過,不會再發第二次請求。

其餘

cookie 的同源策略

另外就是設置了 XMLHttpRequest 的 withCredentials 屬性爲 true,瀏覽器發出去了,服務端仍是拿不到 cookie的問題。

cookie 也遵循同源策略的,在設置 cookie 的時候能夠發現除了鍵值對,還能夠設置 cookie 的這些值:

cookie屬性值 說明
path 可訪問 cookie 的路徑,默認爲當前文檔位置的路徑
domain 可訪問 cookie 的域名,默認爲當前文檔位置的路徑的域名部分
max-age 多久後失效,秒爲單位時間。
負數:session 內有效;0:刪除 cookie;正數:有效期爲建立時刻 + max-age
expires cookie 失效日期.若是沒有定義,cookie 會在對話結束時過時,即會話 cookie
secure cookie 只經過 https 協議傳輸

若是獲取不到 cookie,能夠檢查下 cookie 的 domain 和 path.

IE 上跨域訪問沒有權限

在跨域發送 ajax 請求時提示沒有權限。 由於IE瀏覽器默認對跨域訪問有限制。須要在瀏覽器設置中去除限制。
方法: 設置 > Internet 選項 > 安全 > 自定義級別 > 在設置中找到其餘 - 在【其餘】中將【經過域訪問數據源】啓用。

Demo 源碼

CORS Demo 源碼

參考

相關文章
相關標籤/搜索