天天看點

ajax、axios請求之同源政策與CORS

什麼是同源政策?

同源政策(Same origin policy)是一種約定,它是浏覽器最核心也最基本的安全功能,如果缺少了同源政策,則浏覽器的正常功能可能都會受到影響。可以說Web是建構在同源政策基礎之上的,浏覽器隻是針對同源政策的一種實作。

同源政策,它是由Netscape提出的一個著名的安全政策。現在所有支援JavaScript 的浏覽器都會使用這個政策。所謂同源是指,域名,協定,端口相同。當一個浏覽器的兩個tab頁中分别打開來 百度和谷歌的頁面當浏覽器的百度tab頁執行一個腳本的時候會檢查這個腳本是屬于哪個頁面的,即檢查是否同源,隻有和百度同源的腳本才會被執行。如果非同源,那麼在請求資料時,浏覽器會在控制台中報一個異常,提示拒絕通路。

注意:跨域請求被拒絕,其實是浏覽器已經拿到了不同源伺服器響應的資料,浏覽器對非同源請求傳回的結果做了攔截,而不是伺服器拒絕浏覽器的請求。

ajax跨域

<script>
    $("button").click(function(){
        $.ajax({
            url:"http://127.0.0.1:7766/order/",  //假設網頁的服務時http://127.0.0.1:8000,此時ajax去請求7766端口的服務
            type:"POST",
            success:function(data){
                alert(123);
                alert(data)
            }
        })
    })
</script>      

上面這種請求方式設計跨域請求,會報如下錯誤:

已攔截跨源請求:同源政策禁止讀取位于 http://127.0.0.1:7766/order/ 的遠端資源。(原因:CORS 頭缺少 'Access-Control-Allow-Origin')。      

解決辦法下文會深入讨論。

什麼是CORS?

CORS需要浏覽器和伺服器同時支援。目前,所有浏覽器都支援該功能,IE浏覽器不能低于IE10。

整個CORS通信過程,都是浏覽器自動完成,不需要使用者參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差别,代碼完全一樣。浏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。

是以,實作CORS通信的關鍵是伺服器。隻要伺服器實作了CORS接口,就可以跨源通信。

CORS兩種請求

浏覽器将CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

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

(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      

凡是不同時滿足上面兩個條件,就屬于非簡單請求。

浏覽器對這兩種請求的處理,是不一樣的。

* 簡單請求和非簡單請求的差別?
   簡單請求:一次請求
   非簡單請求:兩次請求,在發送資料之前會先發一次請求用于做“預檢”,隻有“預檢”通過後才再發送一次請求用于資料傳輸。

* 關于“預檢”
- 請求方式:OPTIONS
- “預檢”其實做檢查,檢查如果通過則允許傳輸資料,檢查不通過則不再發送真正想要發送的消息
- 如何“預檢”
     => 如果複雜請求是PUT等請求,則服務端需要設定允許某請求,否則“預檢”不通過
        Access-Control-Request-Method
     => 如果複雜請求設定了請求頭,則服務端需要設定允許某請求頭,否則“預檢”不通過
        Access-Control-Request-Headers      

支援跨域,簡單請求

伺服器設定響應頭即可:Access-Control-Allow-Origin = '域名' 或 '*'

支援跨域,複雜請求

由于複雜請求時,首先會發送“預檢”請求,如果“預檢”成功,則發送真實資料。

  • “預檢”請求時,允許請求方式則需伺服器設定響應頭:Access-Control-Request-Method
  • “預檢”請求時,允許請求頭則需伺服器設定響應頭:Access-Control-Request-Headers

複雜請求下,django的響應頭設定,如下:

在設定了rest_framework情況下,必須要有的是options請求的方法

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
import json

class Order(APIView):
    def options(self, request, *args, **kwargs):
        response = HttpResponse("ok")
        response['Access-Control-Allow-Origin'] = '*'  # 允許所有的域名位址
        response["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS,PATCH,PUT"  # 允許的請求方式
        response["Access-Control-Allow-Headers"] = "Content-Type"  # 允許的headers
        return response      

因為複雜請求都會請求多次,第一次一定是OPTIONS請求,也就是預檢。如果預檢通過就會攜帶資料進行再次請求,這時需要根據請求方式增加對應的處理方法。

比如,預檢通過之後進行GET請求:

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
import json

class Order(APIView):
    def options(self, request, *args, **kwargs):
        response = HttpResponse("ok")
        response['Access-Control-Allow-Origin'] = '*'  # 允許所有的域名位址
        response["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS,PATCH,PUT"  # 允許的請求方式
        response["Access-Control-Allow-Headers"] = "Content-Type"  # 允許的headers
        return response

    def get(self, request, *args, **kwargs):
        response = HttpResponse("ok")
        response['Access-Control-Allow-Origin'] = '*'  # 允許所有的域名位址
        response["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS,PATCH,PUT"  # 允許的請求方式
        response["Access-Control-Allow-Headers"] = "Content-Type"  # 允許的headers
        return response      

ajax自定義headers

<script>
    $("button").click(function(){
        $.ajax({
            url:"http://127.0.0.1:7766/order/",  //假設網頁的服務時http://127.0.0.1:8000,此時ajax去請求7766端口的服務
            type:"POST",
            headers: {k1: "v1"},  // 自定義headers,這裡設定k1,服務端就要允許k1
            contentType: "application/json"
            success:function(data){
                alert(123);
                alert(data)
            }
        })
    })
</script>      

服務端django設定headers

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
import json

class Order(APIView):
    def options(self, request, *args, **kwargs):
        response = HttpResponse("ok")
        response['Access-Control-Allow-Origin'] = '*'  # 允許所有的域名位址
        response["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS,PATCH,PUT"  # 允許的請求方式
        response["Access-Control-Allow-Headers"] = "Content-Type, k1"  # 允許的headers,添加k1
        return response

    def get(self, request, *args, **kwargs):
        response = HttpResponse("ok")
        response['Access-Control-Allow-Origin'] = '*'  # 允許所有的域名位址
        response["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS,PATCH,PUT"  # 允許的請求方式
        response["Access-Control-Allow-Headers"] = "Content-Type, k1"  # 允許的headers,添加k1
        return response      

axios跨域

import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
axios.defaults.baseURL= '/api'

// 建立axios執行個體
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url, base_url="http://127.0.0.1:80001"
  timeout: 15000 // 請求逾時時間
})

// request攔截器
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['Authorization'] = getToken() // 讓每個請求攜帶自定義token 請根據實際情況自行修改
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone攔截器
service.interceptors.response.use(
  response => {
  /**
  * code為非200是抛錯 可結合自己業務進行修改
  */
    const res = response.data
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 3 * 1000
      })

      // 401:未登入;
      if (res.code === 401) {
        MessageBox.confirm('你已被登出,可以取消繼續留在該頁面,或者重新登入', '确定登出', {
          confirmButtonText: '重新登入',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload()// 為了重新執行個體化vue-router對象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      return response.data
    }
  },
  error => {
    console.log('err' + error)// for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 3 * 1000
    })
    return Promise.reject(error)
  }
)

export default service      

axios在前端伺服器如果沒有設定特殊全局headers,對應的後端伺服器設定headers同上文。

結束!