记录一次设置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
posted @ 2021-05-31 22:01  小云菜  阅读(1048)  评论(0编辑  收藏  举报