從現在開始認識跨域

同源策略

同源策略(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 請求去檢測該請求是否跨域,跨域通過纔會發送非簡單請求去服務端執行。

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

  1. 請求方法是以下三種:
    • HEAD
    • GET
    • POST
  2. HTTP的頭信息不超出以下幾種字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/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);
    }

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章