zoukankan      html  css  js  c++  java
  • 记录一次设置xhr.widthCredentials=true失效原因的查找过程

    我们项目库内使用axios作为网络请求库,在开发环境下,配置widthCredentials = true后,发现网络请求头内并没有携带cookie

    正常的情况,应该是下面这样的

    Request Headers
      GET /api.json HTTP/1.1
      Host: api.topLevelDomain.com
      Cookie: xxxxxx
    

    关于XMLHttpRequest.withCredentials:如果在发送来自其他域的xhr请求之前,未设置witCredentials = true,那么就不能为它自己的域设置cookie值。而通过设置其为true后,获得的第三方cookies,将依旧会享受同源策略。具体信息可参考

    首先复查了一下代码

    Axios.defaults.withCredentials = true
    // 为了保险起见,发起请求时,也主动带上了配置项
    await axiosInstance.get(url, {withCredentials: true})
    

    代码没问题

    接着确认开发环境下配置的域名

    dev.topLevelDomain.com与接口域名是在同一个一级域名下,域名本身是没有问题的(为什么要确认在同一个一级域下,实际上即使配置了相关的跨域策略,IOS平台也有大概率会失效,接下来会单独写一篇博文聊这个问题

    然后排查服务端接口有无做跨域配置

    经排查,也是正确的

    Response Headers
      HTTP/1.1 200 OK
      Server: nginx
      Content-Type: application/json;charset=utf-8
      Access-Control-Allow-Origin: http://dev.topLevelDomain.com
      Access-Control-Allow-Credentials: true
    

    遇事不决,debugger

    当代码执行到如下行数时,发现配置项未生效。config.withCredentials为false

    难道是axios本身的bug?

        // axios/lib/adapters/xhr.js line 140
        // Add withCredentials to request if needed
        if (config.withCredentials) {
          request.withCredentials = true
        }
    

    是代码执行顺序的问题么?

    把上面三行代码提到上面最上面看看呢

    module.exports = function xhrAdapter(config) {
      return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data
        var requestHeaders = config.headers
    
        if (utils.isFormData(requestData)) {
          delete requestHeaders['Content-Type'] // Let the browser set it
        }
    
        var request = new XMLHttpRequest()
        // 移动到这里
        if (config.withCredentials) {
          request.withCredentials = true;
        }
        // HTTP basic authentication
    

    刷新页面后,这次对了,Request headers内有cookie

    不应该啊? 如果是代码顺序的问题,github官方库issue内有人反馈啊,我查了一下,并没有

    是版本的问题么?axios当前版本是0.18.0,升级到最新版本0.21.1试试

    还是不可以.......

    这就奇怪了,我决定直接用xhr请求试试

    使用xhr测试

      const xhr = new XMLHttpRequest()
      xhr.open('GET',url)
      xhr.withCredentials = true
      xhr.send()
    

    不可以,请求头内依然不携带cooke

    难道执行xhr.open后,再去配置withCredentials是不正确的?

    试试更改下代码顺序

      const xhr = new XMLHttpRequest()
      xhr.withCredentials = true
      xhr.open('GET',url)
      xhr.send()
    

    这样,居然可以!!!

    此刻我对chromexhr陷入了深深的怀疑--------

    换个浏览器将上面两个请求重新试试看,firefox, ie11同样的结果,在手机端ios safari也是一样的情况,

    不可能吧,难道这么多浏览器厂商都没发现这个问题~~~~

    看一下mdn文档,官方实例也未强调先后顺序的问题

    此刻我陷入了深深的自我否定中.......

    我就不信邪了,单独起个koa服务试试

    脱离当前开发环境,新起koa服务

    • 配两个host
    172.0.0.1 dev.phillyx.com dev2.phillyx.com
    
    • 起一个koa服务
    module.exports = {
      'GET /credentials': async (ctx, next) => {
        try {
          ctx.set('Access-Control-Allow-Credentials',true)
          ctx.set('Access-Control-Allow-Origin','http://dev2.phillyx.com')
          ctx.response.body = {
            code:1,
            msg:'widthCredentials: true'
          }
        } catch (error) {
          console.log(error)
          ctx.response.body = error
        }
      }
    }
    
    • 单独请求localhost:3000/credentials,ok 代码验证通过

    • 配一下nginx代理

    http{
      server{
        listen 80;
        location / {
            root   ./code;
            index  index.html index.htm;
        }
        location /credentials {
            proxy_set_header X-Forwarded-For $proxy_add_x_Forwarded_for;
            proxy_pass http://127.0.0.1:3000/credentials;
            proxy_set_header Host $host;
        }
      }
    }
    
    • /code目录下新建一个test-credentials.html
    <body>
      <script>
        (() => {
          const xhr = new XMLHttpRequest()
          xhr.open('GET', 'http://dev.phillyx.com/credentials')
          xhr.withCredentials = true
          xhr.send()
        })()
      </script>
    </body>
    
    • 发起请求http://dev2.phillyx.com/test-credentials.html
    • 经测试与xhr.withCredentials = true顺序无关
    Response Headers
      HTTP/1.1 200 OK
      Server: nginx/1.19.2
      Date: XXXX
      Content-Type: application/json; charset=utf-8
      Content-Length: 41
      Connection: keep-alive
      Access-Control-Allow-Credentials: true
      Access-Control-Allow-Origin: http://dev2.phillyx.com
      X-Response-Time: 1ms
      Cache-Control: no-store
    
    Request Headers
      GET /credentials HTTP/1.1
      Host: dev.phillyx.com
      Connection: keep-alive
      Pragma: no-cache
      Cache-Control: no-cache
      User-Agent: xxxxx
      Accept: */*
      Origin: http://dev2.phillyx.com
      Referer: http://dev2.phillyx.com/
      Accept-Encoding: gzip, deflate
      Accept-Language: zh-CN,zh;q=0.9
      Cookie: xxxxxxx
    
    • 石锤了,代码中哪一块肯定有问题

    此刻我灵光一闪,犹如码神附体,为什么不从代码调用栈开始看呢

    打开控制台-->initiator-->查看调用顺序,果不其然

    initiator

    都是mock.js的锅

    内鬼查出来了,此刻我不知道是该高兴还是悲伤~

    为什么刚开始没想到!!!

    • 项目中mock.js的版本为1.1.0
    • mock.js全局拦截xhr
    // line 9
    var XHR
    if (typeof window !== 'undefined') XHR = require('./mock/xhr')
    // node_modules/mockjs/src/mock/mock.js
    Mock.mock = function(rurl, rtype, template) {
        // ...
        // line 58
        // 拦截 XHR
        if (XHR) window.XMLHttpRequest = XHR
        // ...
        return Mock
    }
    
    module.exports = Mock
    
    // node_modules/mockjs/src/mock/xhr/xhr.js line244
    setRequestHeader: function(name, value) {
        // ...
        // line 257
        withCredentials: false,
        // https://xhr.spec.whatwg.org/#the-send()-method
        // Initiates the request.
        send: function send(data) {
            // 原生 XHR
            if (!this.match) {
                this.custom.xhr.send(data)
                return
            }
    
    • 在官方issues内也有同学反馈了这个bug,mock/issues/300

    • 修复方法也很简单,删掉或添加一个全局配置

    Mock.XHR.prototype.withCredentials = true
    

    but, 为什么xhr.withCredentials放在xhr.open()方法之前有效呢

    fix mock.js

    • 上文我们提到mock.js全局托管了xhr, 当我们var xhr = new XMLHttpRequest()时,新建的是MockXMLHttpRequest对象
    • 我们执行xhr.withCredentials = true的赋值语句时,实际上是如下的关系
     obj.withCredentials = true
     // but
     obj.prototype.withCredentials === false
     // 这是在 node_modules/mockjs/src/mock/xhr/xhr.js line 257 写死了的
     withCredentials: false,
     // 取值顺序 this.a > this.prototype.a
    
    • 之所以xhr.withCredentials = true写在xhr.open()之前生效,是因为下方line226行for循环在起作用
    • 同理,将xhr.withCredentials = true写在xhr.open()之后,并不会执行将自定义属性赋值给原生的过程
    • 所以修复这个bug也很简单了,移动for循环send方法相关条件分支内
      // node_modules/mockjs/src/mock/xhr/xhr.js
      var XHR_REQUEST_PROPERTIES = 'timeout withCredentials'.split(' ')
      // line 167
      Util.extend(MockXMLHttpRequest.prototype, {
        // https://xhr.spec.whatwg.org/#the-open()-method
        // Sets the request method, request URL, and synchronous flag.
        open: function(method, url, async, username, password) {
        // ...
        // line211
        // 如果未找到匹配的数据模板,则采用原生 XHR 发送请求。
          if (!item) {
            // 创建原生 XHR 对象,调用原生 open(),监听所有原生事件
            var xhr = createNativeXMLHttpRequest()
            this.custom.xhr = xhr
            // ...
            // line226 
            // 同步属性 MockXMLHttpRequest => NativeXMLHttpRequest
            // this.obj
            for (var j = 0; j < XHR_REQUEST_PROPERTIES.length; j++) {
                try {
                    xhr[XHR_REQUEST_PROPERTIES[j]] = that[XHR_REQUEST_PROPERTIES[j]]
                } catch (e) {}
            }
    
            return
          }
      }
    
    • 已提交官方pull request

    By小云菜:http://www.cnblogs.com/phillyx/

    github:http://github.com/phillyx

    版权所有,转载请注明出处

  • 相关阅读:
    7-4
    7-3
    第五章例5-2
    第五章例5-1
    第四章例4-12
    第四章例4-11
    第四章例4-10
    第四章例4-9
    第四章例4-8
    第四章例4-7
  • 原文地址:https://www.cnblogs.com/phillyx/p/14833536.html
Copyright © 2011-2022 走看看