同源策略
同源策略(Same origin policy)是一種約定,是有 Netscape 提出的一個著名的安全策略。所謂 同源 指的是 域名,協議,端口相同。同源策略是瀏覽器的行爲,是爲了保護本地數據不被JavaScript代碼獲取回來的數據污染,因此攔截的是客戶端發出的請求回來的數據接收,即請求發送了,服務器響應了,但是無法被瀏覽器接收。瀏覽器如果檢查到資源屬於非同源資源時,瀏覽器會在控制檯中報一個異常,提示拒絕訪問。
什麼纔算是同源?
請求方 | 被請求方 | 是否跨域 | 詳細說明 |
---|---|---|---|
http://www.heheda.com | http://www.heheda.com/app.js | 否 | 屬於同源 |
http://www.heheda.com | https://www.heheda.com/app.js | 是 | 協議不同( http | https ) |
http://www.heheda.com | http://blog.heheda.com/app.js | 是 | 子域名不同( www | blog ) |
http://www.heheda.com | http://www.hehe.com | 是 | 主域名不同( heheda | hehe ) |
http://www.heheda.com:8080 | http://www.heheda.com:8081 | 是 | 端口不同( 8080 | 8081 ) |
http://www.heheda.com | http://103.12.13.99 | 是 | 域名和通過 DNS 解析的 IP 也算跨域 |
什麼情況下 AJAX 會產生跨域( 同時滿足 )?
- 瀏覽器限制
- 跨域
- XHR (XMLHttpRequest)請求
準備工作
在 localhost:1314 後臺準備一個接口
@RequestMapping(value = "testString")
public String testString(){
log.info("request success");
return "testString";
}
在 localhost:3000 開啓一個 react 前端,並使用 axios 向後臺發出請求
import React, {Component} from 'react';
import Axios from 'axios'
class Test extends Component{
constructor(){
super();
this.state = {
string: ''
}
}
// 在第一次渲染後調用
componentDidMount(){
const _this = this;
Axios.get("http://localhost:1314/testString").then(function(response){
_this.setState({
string: response.data
})
})
}
render(){
return(
<h1>{this.state.string}</h1>
)
}
}
export default Test;
此時就會出現跨域問題,但是我們的請求是成功的
而瀏覽器的控制檯打出了一個錯誤日誌,這裏可以發現,跨域是瀏覽器前臺做的處理
針對瀏覽器的處理方法
我們讓瀏覽器禁止同源策略來解決跨域問題,不過這是客戶端的處理,存在一定的專業性,對用戶很不友好。
例如 chrome 可以使用 --disable-web-security 參數來進行啓動,禁止同源策略
這個時候再去訪問 localhost:3000,成功跨域訪問 後臺接口
針對 XHR 請求的處理方法
當請求類型是 XHR 的時候後,就會出現跨域問題
這種情況下可以使用 JSONP 來解決,以下是 JSONP 的引用
JSONP(JSON with Padding)是 JSON 的一種“使用模式”,可用於解決主流瀏覽器的跨域數據訪問的問題。由於同源策略,一般來說位於 server1.example.com 的網頁無法與不是 server1.example.com的服務器溝通,而 HTML 的
由於 axios 對 jsonp 不太支持,這裏前端將會切換請求庫 fetch-jsonp,同時改造後臺接口
@RequestMapping(value = "testJSONP")
public JSONPObject testJSONP(HttpServletRequest httpServletRequest){
String callback = httpServletRequest.getParameter("callback");
log.info("request JSONP success");
return new JSONPObject(callback,"testJSONP");
}
使用 fetch-jsonp 發送請求
import React, {Component} from 'react';
import Axios from 'axios'
import fetchJSONP from 'fetch-jsonp'
class Test extends Component{
constructor(){
super();
this.state = {
string: ''
}
}
// 在第一次渲染後調用
componentDidMount(){
const _this = this;
fetchJSONP("http://localhost:1314/testJSONP").then(function(response){
return response.json()
}).then(function(json){
_this.setState({
string: json
})
}).catch(function(error){
console.log(error)
})
}
render(){
return(
<h1>{this.state.string}</h1>
)
}
}
export default Test;
此時發送請求就可以看到該請求不再是 XHR,而是 js
JSONP 請求後面會自動加上 callback,後臺接收到 callback 參數,就會識別這是一個 jsonp,就會將返回值從 json 轉換爲 JS,至於約定的 callback 參數是可以更改的,但是需要後臺進行重寫 JSONP 基類進行修改。同時也需要修改 fetch-jsonp,默認是發送 callback 參數
服務端支持跨域
讓你的服務器告訴瀏覽器支持跨域
當瀏覽器檢測到請求的目的地和來源地非同源時,會自動在請求頭加上請求的來源IP( Origin )
這個服務端可以在響應頭中添加支持跨域的 Origin 的信息( Access-Control-Allow-Origin )來告訴瀏覽器,服務端支持以下 IP 的跨域請求,不包含在響應頭中 IP 來源的請求將會被視爲跨域請求
我們可以在後臺使用過濾器來添加響應頭信息,告訴瀏覽器 支持跨域的 IP 來解決跨域問題
// 該過濾器會添加響應頭信息
@Component
public class CorsFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 支持前端的跨域請求
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
filterChain.doFilter(servletRequest, servletResponse);
}
}
可以發現此時的前端的跨域請求成功了,並且響應頭多了 Access-Control-Allow-Origin 的信息
這樣有一個問題,就是必須要手動在服務器後臺的過濾器添加允許的 IP 才能跨域請求,這樣不僅添加了工作量,也是不現實,我們可以使用 response.setHeader("Access-Control-Allow-Origin", "*")
來表示匹配所有 IP
理解 Options 預檢命令
首先需要了解一下 簡單請求 和 非簡單請求,如果一個請求是簡單請求,則瀏覽器會直接發送請求讓服務器執行再去判斷該請求是否跨域,如果是非簡單請求,則會先發送一個 Options 請求去檢測該請求是否跨域,跨域通過纔會發送非簡單請求去服務端執行。
只要同時滿足以下兩大條件,就屬於簡單請求:
- 請求方法是以下三種:
- HEAD
- GET
- POST
- HTTP的頭信息不超出以下幾種字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不同時滿足上面兩個條件,就屬於非簡單請求
後臺開發一個支持 PUT 請求接口用於測試
@PutMapping(value = "testPut")
public String testPut(){
log.info("testPut");
return "testPut";
}
前端發送請求
Axios.put("http://localhost:1314/testPut").then(function(response){
_this.setState({
string: response.data
})
})
由於 PUT 請求不屬於簡單請求,這個時候瀏覽器會先發送一個預檢命令 Options 來測試是否跨域
由於服務器沒有返回支持的 PUT 請求方式的響應頭信息,瀏覽器認定該請求跨域,請求失敗
同樣的需要在服務器端進行處理,添加支持 PUT 方法的響應頭信息,對應的 Key 爲 Access-Control-Allow-Methods
@Component
public class CorsFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
filterChain.doFilter(servletRequest, servletResponse);
}
}
此時會發現前面的 PUT 請求在預檢命令 Options 通過後,發送了 PUT 請求到後臺執行,最終跨域請求成功
如果是HTTP的頭信息包含額外信息導致請求爲非簡單請求,則對應需要添加的響應頭信息是 Access-Control-Allow-Headers
,同時可以使用 Access-Control-Max-Age
來設置 Options 預檢命令的緩存時長,在緩存期內,同一個 Origin 不會每次非簡單請求都先發送預檢命令
@Component
public class CorsFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
// 設置預檢命令緩存時長,單位爲 秒
response.setHeader("Access-Control-Max-Age", "3600");
// 允許自帶請求頭 Content-Type 的請求,這個時候就可以發送 Content-Type 爲 application/json 的請求
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
filterChain.doFilter(servletRequest, servletResponse);
}
}
攜帶 Cookie 的請求需要做額外處理
在很多情況下,我們的請求是需要攜帶 Cookie,常見於攜帶權限認證用的 Token,SessionId 等,如果這個時候是跨域請求,則需要作出額外的處理:
- 前端設置
withCredentials: true
,告訴瀏覽器請求攜帶 cookie - 服務器設置響應頭含有
Access-Control-Allow-Headers
爲 true,通知瀏覽器接收攜帶 cookie 的跨域請求 - 服務器設置響應頭
Access-Control-Allow-Origin
不能再是 *
後臺開發返回 Cookie 信息接口
@GetMapping(value = "testCookie")
public String testCookie(HttpServletRequest httpServletRequest){
Cookie[] cookies = httpServletRequest.getCookies();
if(cookies != null){
for(Cookie cookie : cookies){
if(cookie.getName().equals("token")){
log.info("token : {}",cookie.getValue());
return cookie.getValue();
}
}
}
return null;
}
後臺添加 Access-Control-Allow-Headers
@Component
public class CorsFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
response.setHeader("Access-Control-Allow-Credentials","true");
filterChain.doFilter(servletRequest, servletResponse);
}
前端添加 Cookie
前端發送請求
componentDidMount(){
const _this = this;
const config = {
withCredentials : true
}
Axios.get("http://localhost:1314/testCookie", config).then(function(response){
_this.setState({
string: response.data
})
})
}
如果 服務端設置 Access-Control-Allow-Origin
爲 true,則會跨域請求會被拒絕
將 Access-Control-Allow-Origin
設置爲 具體地址來完成跨域請求
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
response.setHeader("Access-Control-Allow-Credentials","true");
filterChain.doFilter(servletRequest, servletResponse);
}
此時獲取 Cookie 信息成功
我們使用一種 騷操作 來實現接收所有 Origin 跨域請求,可以先獲取請求攜帶的 Origin,在添加到 Access-Control-Allow-Origin
當中
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 先獲取請求頭中的 Origin,在動態添加 響應頭的 Access-Control-Allow-Origin
String originHeader=((HttpServletRequest)servletRequest).getHeader("Origin");
if(originHeader != null){
response.setHeader("Access-Control-Allow-Origin", originHeader);
}
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
response.setHeader("Access-Control-Allow-Credentials","true");
filterChain.doFilter(servletRequest, servletResponse);
}