未分类

axios-cookie问题和表单上传问题探究

自从入了 Vue 之后,一直在用 axios 这个库来做一些异步请求。最近在跨域、cookie 以及表单上传这几个方面遇到了点小问题,做个简单探究和总结。本文将涉及使用 axios 在跨域情况下成功得到响应报文中的内容以及让 cookie 成功设置的解决办法;使用 axios 上传表单数据时候遇到的小问题。

首先来看一个同域情况下的cookie例子清楚整个流程以便后面对跨域情况能够更好地阐述。服务端的代码如下,访问/get-cookie接口的时候,通过session随便设置字段time,使得响应报文中带有set-cookie字段,在浏览器段设置cookie,这个cookie的内容即相应的sessionId。然后访问/test-cookie可以判断浏览器端的 cookie是否设置成功,如果设置成功,那么浏览器发送的报文会自动带上cookie字段,服务器也就可以根据cookie找到对应的session。如果设置失败,那么浏览器发送的报文不会带上cookie字段,服务器也就无法找到对应的session,从而返回error

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
const express = require('express')
const path = require('path')
const cookieParser = require('cookie-parser')
const session = require('express-session')
const FileStore = require('session-file-store')(session)

let app = express()

app.use('/static', express.static(path.join(__dirname, './static')))
app.use(cookieParser())
app.use(session({
store: new FileStore(),
secret: 'hongchh',
resave: false,
saveUninitialized: false
}))

app.use('/get-cookie', (req, res) => {
req.session.time = Date.now()
res.json({ result: 'ok' })
})

app.use('/test-cookie', (req, res) => {
if (req.session.time) {
console.log('session.time: ' + req.session.time)
console.log(req.cookies)
res.json({ result: 'ok' })
} else {
res.json({ result: 'error' })
}
})

app.listen(8000)

console.log('http://localhost:8000/static/index.html')

前端的代码如下,前端只有两个按钮,触发点击事件分别可以调用两个接口。

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
window.onload = function () {
'usr strict'

document.getElementById('get-cookie').addEventListener('click', getCookie, false)
document.getElementById('test-cookie').addEventListener('click', testCookie, false)

var getResult = document.getElementById('get-result')
var testResult = document.getElementById('test-result')

function getCookie() {
axios.get('/get-cookie').then(function (res) {
if (res.status === 200) {
getResult.textContent = res.data.result
} else {
getResult.textContent = 'ERROR'
}
})
}

function testCookie() {
axios.get('/test-cookie').then(function (res) {
if (res.status === 200) {
testResult.textContent = res.data.result
} else {
testResult.textContent = 'ERROR'
}
})
}
}

首先点击get-cookie按钮,再点击test-cookie按钮,发送的报文信息如下,可以看到,get-cookie的响应报文中有set-cookie字段,而test-cookie的请求报文中自动带上了cookie字段。通过 Chrome 的开发者工具也可以看到设置cookie成功。服务端也输出了相应的sessioncookie的信息。

get-cookie

test-cookie

cookie

终端输出

为了实现跨域,将服务器拆分成2个服务器,1个提供静态资源,1个提供接口。访问8002端口的服务器加载前端的界面,然后跨域访问8001端口的服务器调用get-cookietest-cookie接口。为了满足跨域,8001服务器的响应报文里面应该有Access-Control-Allow-Origin字段,字段值设置为'*'表示满足所有其他域的访问。

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
const express = require('express')
const cookieParser = require('cookie-parser')
const session = require('express-session')
const FileStore = require('session-file-store')(session)

let app = express()

app.use(cookieParser())
app.use(session({
store: new FileStore(),
secret: 'hongchh',
resave: false,
saveUninitialized: false
}))

app.use('/get-cookie', (req, res) => {
req.session.time = Date.now()
res.header('Access-Control-Allow-Origin', '*')
res.json({ result: 'ok' })
})

app.use('/test-cookie', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
if (req.session.time) {
console.log('session.time: ' + req.session.time)
console.log(req.cookies)
res.json({ result: 'ok' })
} else {
res.json({ result: 'error' })
}
})

app.listen(8001)

console.log('http://localhost:8001/')

1
2
3
4
5
6
7
8
9
const express = require('express')
const path = require('path')

let app = express()

app.use('/static', express.static(path.join(__dirname, './static')))
app.listen(8002)

console.log('http://localhost:8002/')

前端只需要小做修改,将调用接口的 URL 改为绝对地址。下面是get-cookie的写法,test-cookie也类似。

1
2
3
4
5
6
7
8
9
function getCookie() {
axios.get('http://localhost:8001/get-cookie').then(function (res) {
if (res.status === 200) {
getResult.textContent = res.data.result
} else {
getResult.textContent = 'ERROR'
}
})
}

同样地,先get-cookie,然后test-cookie,得到的结果如下。访问没有出错,但是也没有成功设置cookie。可以看到get-cookie的响应报文里面是有set-cookie字段的,但是test-cookie的请求报文并没有带上cookie字段。通过 Chrome 的开发者工具也可以看到cookie没有设置成功。

get-cookie

test-cookie

cookie

查看axios的文档之后,发现需要配置withCredentials属性,在全局配置axioswithCredentials属性为true

1
axios.defaults.withCredentials = true

配置完成之后再进行测试,得到的结果如下。确实成功设置了cookietest-cookie的请求报文会自动带上cookie。但是前端除了成功设置cookie之外,还会报错,而且无法读取到响应报文的主体部分的内容。

get-cookie

test-cookie

报错信息

无法读取响应内容

根据错误提示,应该是说不能直接将Access-Control-Allow-Origin字段的值设置为'*'。因此,我们直接将其值修改为确定的域名试试。对get-cookietest-cookie作如下修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use('/get-cookie', (req, res) => {
req.session.time = Date.now()
res.header('Access-Control-Allow-Origin', 'http://localhost:8002')
res.json({ result: 'ok' })
})

app.use('/test-cookie', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:8002')
if (req.session.time) {
console.log('session.time: ' + req.session.time)
console.log(req.cookies)
res.json({ result: 'ok' })
} else {
res.json({ result: 'error' })
}
})

继续进行测试,得到的结果跟之前相似,成功设置了cookie,但却无法获取响应报文的主体部分的内容,并且继续报错了。其他内容相似这里不重复截图。只有这个报错的信息是不同的,如下图所示。看样子修改为确定的域名成功解决了之前那个问题,接下来需要解决这新的问题。

报错信息

根据报错提示,是说除了Access-Control-Allow-Origin字段之外,我们还需要设置Access-Control-Allow-Credentials的值为true。根据提示对服务端代码进行修改,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.use('/get-cookie', (req, res) => {
req.session.time = Date.now()
res.header('Access-Control-Allow-Origin', 'http://localhost:8002')
res.header('Access-Control-Allow-Credentials', 'true')
res.json({ result: 'ok' })
})

app.use('/test-cookie', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:8002')
res.header('Access-Control-Allow-Credentials', 'true')
if (req.session.time) {
console.log('session.time: ' + req.session.time)
console.log(req.cookies)
res.json({ result: 'ok' })
} else {
res.json({ result: 'error' })
}
})

继续进行测试,得到结果如下。这次除了可以正确设置cookie之外,也可以读取到响应报文的主体部分的内容,并且没有任何报错。

get-cookie

test-cookie

终端输出

因此,可以得到结论,在跨域的情况下使用axios,首先需要配置axioswithCredentials属性为true。然后服务器还需要配置响应报文头部的 Access-Control-Allow-OriginAccess-Control-Allow-Credentials两个字段,Access-Control-Allow-Origin字段的值需要为确定的域名,而不能直接用'*'代替,Access-Control-Allow-Credentials的值需要设置为true。前端和服务端两边都配置完善之后就可以正常进行跨域访问以及携带cookie信息了。

三、表单上传问题

之前在使用axios上传表单数据的过程中,我通常像下面会这么做。

1
2
3
4
5
6
7
8
9
10
var formData = {
key1: 'value1',
key2: 'value2',
key3: 'value3'
}
axios.post('/upload', formData).then(function (res) {
if (res.status === 200) {
// do something
}
})

上面这种做法在NodeJS express服务端使用body-parser可以正确解析到请求报文里面body部分的数据,也就是我提交的表单数据,所以这样玩了很久也没有出现过什么问题。直到最近在课程作业中跟1个Python flask后台的同学协作过程中才开始踩到坑。用上面那种方式提交的数据,在他的后台程序里面提取不到表单数据。他的后台通过 req.form的方式获取表单数据,跟expressreq.body好像有那么一点不同。于是我猜想expressbody-parser中间件是把报文的主体部分的数据都提取了出来,而他的req.form只会提取标准的表单数据。通过修改axios post的数据也可以验证猜想。代码修改如下,使用 js 提供的FormData来包装需要提交的表单数据。最后服务器也成功收到了前端上传的数据。

1
2
3
4
5
6
7
8
9
var formData = new FormData()
formData.append('key1', 'value1')
formData.append('key2', 'value2')
formData.append('key3', 'value3')
axios.post('/upload', formData).then(function (res) {
if (res.status === 200) {
// do something
}
})

这时候大概可以简单理解一下两者区别,第一种方式是 post 了一个 js 对象,会被变成json 字符串的形式提交。而第二种则是提交了一个FormData对象,标准的表单数据对象。因此,如果在 Java、PHP 等其他后台遇到类似的问题时,也可以考虑尝试用FormData对象来解决。

分享到