常常被問到一些問題,好比寫 Java 服務端的同窗的來問:我服務端明明正確返回了,測試環境 debug 能看到,爲何前端就是拿不到數據? 而後寫前端的同窗會問:爲何我明明設置了 withCredentials=true,服務端同窗仍是拿不到 cookie?javascript
因此決定從新捋一捋使用 CORS 解決跨域的問題,先後端要怎麼作?爲何這麼作?html
爲了保證用戶信息的安全,全部的瀏覽器都遵循同源策略,那什麼狀況下算同源呢?同源策略又是什麼呢?前端
記住:協議、域名、端口號徹底相同時,纔是同源java
能夠參考 Web安全 - 瀏覽器的同源策略git
在同源策略下,會有如下限制:github
可是咱們又常常會遇到先後端分離,不在同一個域名下,須要ajax請求數據的狀況。那咱們就要規避這種限制。web
能夠在網上搜到不少解決跨域的方法,有些方法比較古老了,如今項目中用的比較多的是 jsonp 和 CORS(跨域資源共享),這篇主要講 CORS 的原理和具體實踐。ajax
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文件夾下,啓動靜態服務器,前端頁面經過http://localhost:8000/index.html
訪問:
anywhere -h localhost -p 8000
複製代碼
在 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’
在瀏覽器地址欄輸入http://localhost:8000/index.html
,從不一樣域的網頁中向 Server 發送 ajax 請求。能夠看到幾個方面:
從 network 能夠看到,請求返回正常。
但 Response 中沒有內容,顯示 Failed to load response data
。
而且控制檯報錯:
總結:
一、瀏覽器請求是發出去了的,服務端也會正確返回,可是咱們拿不到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 就是告訴瀏覽器容許向服務端請求資源的域名,瀏覽器經過 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
能夠看到咱們可以拿到返回結果了,響應頭中有咱們在服務端設置的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 的請求,詢問服務器是否容許。
在這裏服務器尚未作任何容許這種請求的設置,因此瀏覽器控制檯報錯:
也清楚的說明了出錯的緣由: 服務端在預檢請求的響應中沒有告訴瀏覽器容許協議頭 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() {}
}
複製代碼
能夠看到請求成功
再來看請求的具體信息,第一次以 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 //容許跨域的源
複製代碼
也能夠設置 Access-Control-Allow-Methods 來限制客戶端的的請求方法。
這樣預檢請求成功了,瀏覽器會發出第二個請求,這是真正請求數據的請求:
能夠看到 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.
能夠看到響應頭中有 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 信息
可是服務端返回的數據沒有在頁面中展現,而且報錯:
報錯信息很明白: 當請求中包含憑證信息時,須要設置響應頭 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 中拿到內容了。
那若是附帶憑證信息而且有預檢請求呢?若是有預檢請求,並附帶憑證信息( XMLHttpRequest 的withCredentials 設置爲 true), 服務端須要設置 Access-Control-Allow-Credentials: true,不然瀏覽器不會發出第二次請求,並報錯。
總結:
一、跨域請求時,瀏覽器默認不會發送cookie,須要設置XMLHttpRequest的withCredentials屬性爲true
二、 瀏覽器設置XMLHttpRequest的withCredentials屬性爲true,代表要向服務端發送憑證信息(這裏是cookie)。那麼服務端就須要在響應頭中添加Access-Control-Allow-Credentials爲true。不然瀏覽器上有兩種狀況:
- 若是是簡單請求,服務端結果吐出了,瀏覽器拿到了但就是不給吐出來,並報錯。
- 若是是預檢請求,一樣咱們拿不到返回結果,並報錯提示預檢請求不經過,不會再發第二次請求。
另外就是設置了 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.
在跨域發送 ajax 請求時提示沒有權限。 由於IE瀏覽器默認對跨域訪問有限制。須要在瀏覽器設置中去除限制。
方法: 設置 > Internet 選項 > 安全 > 自定義級別 > 在設置中找到其餘 - 在【其餘】中將【經過域訪問數據源】啓用。