• Ajax跨域请求的两种实现方式


    一、跨域请求痛点

    最近网站新增了一个域名B用于分离不同的功能。但是需要复用服务器的高防等服务,但是服务和原有域名A绑定,所以新域名B需要直接去调用域名A。

    一开始想使用CNAME的方式,让B直接指向A。但是Https支持性有点问题,需要多域名证书。也考虑过反向代理,但是代理服务器的性能和高防等又是一个问题。

    最终决定在域名B的网页中,所有请求都直接去调用域名A的接口。于是就遇到了跨域请求的问题。

    二、跨域请求的实现方式

    网上找了许多资料来实现跨域请求。最终预估下来,有两种方案比较靠谱:通过iframe实现和CORS方案

    三、通过iframe实现

    初步设想是加载一个域名A的iframe页面,然后通过postMessage将所有Ajax请求,转发给这个页面,通过这个页面来进行请求,最终将结果通过postMessage回发给外层的域名B页面。

    于是开始实现:

    前端使用的是React,所以实现了一个FrameHttp.js专门用法封装ajax调用。调用FrameHttp.ajax将所有外部Jquery请求转发给iframe中域名A的/util/ajaxrequest页面。

    import Tools from "../Tools"
    
    const FrameHttpCmd = {
        INIT: 1,
        REQUEST: 2,
        REQUEST_CALLBACK: 3,
    }
    
    class FrameHttp {
        static isInit = false
        static _request_buffer = []
        static frame = null
        static message_key = 0
        static message_key_max = 10000000
        static request_map = {}
    
        static init(domain) {
            var the_frame = document.createElement('iframe')
            let url_obj = new URL(domain)
            url_obj.pathname = Tools.getUrl('/util/ajaxrequest')
            the_frame.src = url_obj.toString()
            the_frame.style.visibility = 'hidden'
            the_frame.style.position = 'absolute'
            the_frame.style.width = 0
            the_frame.style.height = 0
    
            FrameHttp.frame = the_frame
            document.body.appendChild(the_frame)
            window.addEventListener('message', this.onMessage)
        }
    
        static _initFrame() {
            FrameHttp.isInit = true
            console.log('(INFO)FrameHttp._initFrame')
            for (let i = 0; i < FrameHttp._request_buffer.length; i++) {
                let arg = FrameHttp._request_buffer[i]
                FrameHttp.ajax(arg)
            }
            FrameHttp._request_buffer = []
        }
    
        static getMessageKey() {
            let message_key = FrameHttp.message_key
            FrameHttp.message_key = (FrameHttp.message_key+1)%FrameHttp.message_key_max 
            return message_key
        }
    
        static ajax(arg) {
            if (FrameHttp.isInit) {
                // console.log(arg)
                const { success, error, ...others } = arg
                let message_key = FrameHttp.getMessageKey()
                FrameHttp.request_map[message_key] = { success, error }
                FrameHttp.frame.contentWindow.postMessage({
                    cmd: FrameHttpCmd.REQUEST,
                    data: others,
                    key: message_key,
                }, '*')
                console.log('(INFO)FrameHttp.ajax', others)
            }
            else {
                FrameHttp._request_buffer.push(arg)
                console.log('(INFO)FrameHttp.ajax:push buffer')
            }
        }
    
        static onMessage(e) {
            const { data } = e
            if (data.cmd == FrameHttpCmd.INIT) {
                FrameHttp._initFrame()
            }
            else if (data.cmd == FrameHttpCmd.REQUEST_CALLBACK) {
                // console.log(data.key, data.success)
                let item = FrameHttp.request_map[data.key]
                if (data.error) {
                    if (item.error) {
                        item.error(data.error)
                    }
                }
                else {
                    if (item.success) {
                        item.success(data.success)
                    }
                }
                delete FrameHttp.request_map[data.key]
            }
        }
    }
    
    export default FrameHttp
    export { FrameHttpCmd }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    然后域名A中的/util/ajaxrequest页面处理请求:

    import React, { Component } from 'react'
    import { FrameHttpCmd } from '../../../common_js/web_frame/FrameHttp'
    import jquery from '../../../common_js/jquery.min'
    
    class AjaxRequest extends Component {
        constructor(props) {
            super(props)
    
            this.onMessage = this.onMessage.bind(this)
        }
    
        onMessage(e) {
            const { cmd, data, key, cookie } = e.data
            if (cmd == FrameHttpCmd.REQUEST) {
                // console.log(key, data)
                console.log(document.cookie)
                jquery.ajax({
                    ...data,
                    success: (data)=>{
                        window.parent.postMessage({
                            cmd: FrameHttpCmd.REQUEST_CALLBACK,
                            key,
                            success: data,
                        }, '*')
                    },
                    error: ()=>{
                        window.parent.postMessage({
                            cmd: FrameHttpCmd.REQUEST_CALLBACK,
                            key,
                            error: 'error',
                        }, '*')
                    },
                })
            }
        }
    
        componentDidMount() {
            window.parent.postMessage({
                cmd: FrameHttpCmd.INIT,
            }, '*')
            window.addEventListener('message', this.onMessage)
        }
    
        render() {
            return null
        }
    }
                    
    export default AjaxRequest
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    如此实现后,发现iframe因为跨域问题无法加载

    因为后端是由Django实现的,经过查阅发现,在views中需要进行处理,添加xframe_options_exempt装饰器,实现跨域加载iframe。

    @xframe_options_exempt
    def indexCross(request, *args, **kwargs):
        return render(request, 'index.html', {})
    
    • 1
    • 2
    • 3

    功能能够正常请求,但是请求中cookie并没有正常传送。

    经过排查发现,对于跨域的iframe,google浏览器默认对于cookie中SameSite这个参数是LAX,会导致只有同源才能设置服务器返回的Set-Cookie。所以需要将服务器返回的Set-Cookie中指定SameSite为None,这样前端才能成功设置Cookie。

    于是服务端Django引入了中间件django-cookies-samesite。settings.py进行如下修改:

    INSTALLED_APPS = [
        ...
        'corsheaders',
        ...
    ]
    
    # 中间件request按照注册顺序顺序执行,response按照注册顺序倒序执行
    MIDDLEWARE = [
        'django_cookies_samesite.middleware.CookiesSameSite', # 此处response需要最后执行
        ...
    ]
    
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_SAMESITE = 'None'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    至此实现了iframe方式跨域。

    四、CORS方案

    这个需要前端Jquery加入两个参数:

    jquery.ajax({
        ...
        xhrFields: {
            withCredentials: true
        },
        crossDomain: true,
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后服务端需要开启跨域请求支持。Django下引入中间件django-cors-headers。settings.py下如下设置:

    MIDDLEWARE = [
        ...
        'corsheaders.middleware.CorsMiddleware', # 这个位置越高越好,至少要高于CommonMiddleware
        ...
    ]
    
    # 跨域配置
    CORS_ALLOW_CREDENTIALS = True
    # CORS_ORIGIN_ALLOW_ALL = True
    CORS_ORIGIN_WHITELIST = [
        'https://www.xxx.com',# 域名B
    ]
    CORS_ALLOW_METHODS = [
        'DELETE',
        'GET',
        'OPTIONS',
        'PATCH',
        'POST',
        'PUT',
        'VIEW',
    ]
    CORS_ALLOW_HEADERS = [
        'XMLHttpRequest',
        'X_FILENAME',
        'accept-encoding',
        'authorization',
        'content-type',
        'dnt',
        'origin',
        'user-agent',
        'x-csrftoken',
        'x-requested-with',
        'Pragma',
        'cache-control',
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    此方案也需要和iframe一样,解决cookie中SameSite的问题。

    至此CORS方案跨域也实现了。

  • 相关阅读:
    Transformers北大源
    JVM的垃圾收集算法
    【计算机视觉】Graph Models算法介绍合集(四)
    图解redis(二)——持久化篇
    计算机毕业设计:基于HTML学校后台用户登录界面模板源码
    <el-date-picker> 设置默认yyyy-MM-dd以及限制规则
    Hadoop -- NN和2NN的工作机制
    算数运算符 与 逻辑运算符
    我们在讲的 Database Plus,到底能解决什么样的问题?
    C 多维数组
  • 原文地址:https://blog.csdn.net/AndyQsmart/article/details/125442914