記一次 CORS 跨域請求出現 OPTIONS 請求的問題及解決方法


今天前后端在聯調接口的時候,發生了跨域請求資源獲取不到的問題。
首先說明下跨域問題的由來。引自HTTP 訪問控制 的一段話:

當 Web 資源請求由其它域名或端口提供的資源時,會發起跨域 HTTP 請求(cross-origin HTTP request)。
比如,站點 http://domain-a.com 的某 HTML 頁面通過 <img> 的 src 請求 http://domain-b.com/image.jpg。網絡上,很多頁面從其他站點加載各類資源(包括 CSS、圖片、JavaScript 腳本)。
出於安全考慮,瀏覽器會限制腳本中發起的跨域請求。比如,使用 XMLHttpRequest 和 Fetch 發起的 HTTP 請求必須遵循同源策略。因此,Web 應用通過 XMLHttpRequest 對象或 Fetch 僅能向同域資源發起 HTTP 請求。 

既然知道了導致問題的原因,就開始解決吧。
筆者使用的 是 Django 框架。github 上面已經有人分享了解決辦法,就是 django-cors-headers。我們直接

pip install django-cors-headers 

安裝一下唄。
安裝好了以后,需要我們去 settings 文件中去配置一下。常見的配置如下:

  • 先在 INSTALLED_APPS 中引入 corsheaders:
INSTALLED_APPS = (
    ...
    'corsheaders',
    ...
)
  • 接着,在 MIDDLEWARE_CLASSES 里面加入 CorsMiddleware 中間件:
MIDDLEWARE_CLASSES = [
    ...
    'corsheaders.middleware.CorsMiddleware',  # cors
    'django.middleware.common.CommonMiddleware',
    ...
  • 然后,配置下一些基本參數:
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True

一些文章還有配置 CORS_ORIGIN_WHITELIST 參數。

筆者也是看了別人的解決方法,之前也是實踐過了。配置好這三個參數就OK了。本來也以為大工告成了。沒想到,居然沒解決!!!
怎么回事?
通過追蹤請求日志,發現每次客戶端請求接口的時候,都會有一個 OPTIONS 請求。

為什么會有 OPTIONS 請求?
原來,產生 OPTIOINS 請求的原因是:自定義 Headers 頭信息導致的。為了限制接口的訪問,我在 request 中間件里面加了一層過濾,通過判斷 headers 中是否有約定好的字段及其對應的值(比如:key為 aaa, value為 bbb),如果有,就默認可以請求。設置完自定義 header 字段后,問題就出現了:原來的簡單請求會變成預檢請求。

XHR對象對於HTTP跨域請求有三種:簡單請求、Preflighted 請求、Preflighted 認證請求。簡單請求不需要發送OPTIONS嗅探請求,但只能按發送簡單的GET、HEAD或POST請求,且不能自定義HTTP Headers。Preflighted 請求和認證請求,XHR會首先發送一個OPTIONS嗅探請求,然后XHR會根據OPTIONS請求返回的Access-Control-*等頭信息判斷是否有對指定站點的訪問權限,並最終決定是否發送實際請求信息。

瀏覽器會去向 Server 端發送一個 OPTIONS 請求,看 Server 返回的 "Access-Control-Allow-Headers" 是否有自定義的 header 字段。因為我之前沒有返回自定義的字段,所以,默認是不允許的,造成了客戶端沒辦法拿到數據。
既然已經知道了原因,且知道了解決思路,就動手干吧。通過閱讀 django-cors-headers 的源碼后,發現 **corsheaders/middleware.py ** 里面已經有實現了,那就不再重復造輪子了。

def process_response(self, request, response):
        """
        Add the respective CORS headers
        """
        origin = request.META.get('HTTP_ORIGIN')
        if not origin:
            return response

        enabled = getattr(request, '_cors_enabled', None)
        if enabled is None:
            enabled = self.is_enabled(request)

        if not enabled:
            return response

        # todo: check hostname from db instead
        url = urlparse(origin)

        if conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true'

        if (
            not conf.CORS_ORIGIN_ALLOW_ALL and
            not self.origin_found_in_white_lists(origin, url) and
            not self.origin_found_in_model(url) and
            not self.check_signal(request)
        ):
            return response

        if conf.CORS_ORIGIN_ALLOW_ALL and not conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
            patch_vary_headers(response, ['Origin'])

        if len(conf.CORS_EXPOSE_HEADERS):
            response[ACCESS_CONTROL_EXPOSE_HEADERS] = ', '.join(conf.CORS_EXPOSE_HEADERS)

        if request.method == 'OPTIONS':
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ', '.join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ', '.join(conf.CORS_ALLOW_METHODS)
            if conf.CORS_PREFLIGHT_MAX_AGE:
                response[ACCESS_CONTROL_MAX_AGE] = conf.CORS_PREFLIGHT_MAX_AGE

        return response

看完后,發現只要配置下 CORS_ALLOW_HEADERS 就好。

from corsheaders.defaults import default_headers

CORS_ALLOW_HEADERS = default_headers + (
    'aaa'
)

至此,問題就算解決了。

參考鏈接:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM