记录一次设置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()
这样,居然可以!!!
此刻我对chrome
的xhr
陷入了深深的怀疑--------
换个浏览器将上面两个请求重新试试看,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-->查看调用顺序,果不其然
都是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