前端 ajax 请求跨域处理
最近在做一个前后端分离的项目,业务管理平台。一个前端项目要对应多个后端接口地址,免不了各种跨域,查了一些资料,做一个小结。
什么是跨域
或许,什么不是跨域更好来解释:
比如网站A,请求了网站B https://abc.com:8080/api/test
- 协议相同,比如 https
- 域名相同,比如 abc.com(或者是 IP 相同)
- 端口相同,比如 8080
要注意的是,不仅仅是 js 可能跨域,css iframe 都有可能跨域。
跨域主要限制在脚本(js css)请求上,对于 html 中加载资源,不算跨域。
比如 网站A http://abc.com
,有:
<img src="http://12.34.56.78/xx.png">
<script src="https://cdn.abc.com/xx.js"></script>
这些都不算做跨域。
但是上面这个 js,只能往当前网站A 的 http://abc.com
下发送请求,否则就算跨域。
也就是说,当前网站地址是啥,就只能往哪里发送请求(iframe 不再考虑范围内)。
跨域的重点说明:
跨域发出的请求,是默认放行而且可以成功的,因为浏览器不清楚服务器端是否支持跨域。
但是成功的请求,如果跨域了,是不会回调 js 代码的,这个是浏览器的保护机制。所以,下文的预请求原因很关键。
说明
前端的话,主要以 jQuery
或 axios
为例。
由于后端代码实现语言不同,故这里只说接口的响应头应该带有哪些信息,具体实现方法或者框架(库 / 包)请自行搜索。主要关注 cors
headers
关键词。本文以 nodejs 的 koa2
框架为例。
比如 JAVA spring 框架,可以考虑 :
response.setHeader("Access-Control-Allow-Origin", "*")
也可以考虑注解形式(@CrossOrigin
),好像是 4.2 之后的版本开始支持注解。
演示代码在这里:Github
以下约定:
前端页面使用:http://127.0.0.1:3100
后端接口使用:http://localhost:3000
这样故意制造跨域。
此外,本文指的是真真正的的跨域,访问真正的数据接口,而不是 JSONP 接口。
简单的跨域处理
简单的跨域主要指发起了简单的请求。
满足简单请求的要求是:
- 只能是
GET
POST
HEAD
请求方法。而且如果是POST
的话,发送数据类型必须是application/x-www-form-urlencoded
、multipart/form-data
、text/plain
之一,其他类型不可以。 - 不能自定义请求头,比如加上
x-token
什么的。当然也不能带上cookie
。
实现跨域非常简单,后端接口需要返回以下一个响应头即可:
ctx.set('Access-Control-Allow-Origin', 'http://127.0.0.1:3000') // 可以用 * 代替网址
前端代码无需特殊处理,即可正常接收数据。例子参考 DEMO1。
或许你见过 ctx.set('Access-Control-Allow-Methods', 'GET, POST, HEAD, DELETE, OPTIONS')
这种写法,我们这就来说。
上面的简单跨域,只能是 GET
POST
HEAD
方法,如果我是 RESTful 风格的接口,偏偏要用 DELETE
怎么办?
或者,我们交互数据,默认类型不是表单格式 application/x-www-form-urlencoded
,是 application/json
格式?
再或者,需要上送特殊的请求头,比如 x-token
?
这时候,就是非简单的请求了。
高级跨域处理(预请求)
上面说到了非简单请求,这种请求有个特点,要先发送一次请求,查一下服务器支持那些特性。这个是通过 OPTIONS
方法请求出去的。
为什么要有预请求?
假设你要跨域删除一条数据,使用
DELETE
方法。此时你请求发出去了,服务器正常处理删除了数据。但是由于 跨域,导致了前端代码无法成功接收到状态,也就无法进行后续处理,对于操作的用户,不知道是否成功了。
那么,用户可能会反复进行删除,或者进行了更进一步错误的操作。
这就麻烦了,跨域请求发出去了,也成功了,但是前端代码收不到结果...
所以,在发送
DELETE
请求前,先发送一个OPTIONS
方法的请求,确认下能否跨域,如果可以的话,在发送第二条真正的删除请求。否则,第二条干脆就不发送了。这样就不会遇到上面的问题了。
所以,预请求用来查明该站点是否允许跨域请求,这样可以避免跨域请求可能带来的数据破坏。
这种情况实现也还好,不过要注意需要实现 OPTIONS
方法:
router.options('/deleteData', (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', 'http://127.0.0.1:3000')
ctx.set('Access-Control-Allow-Methods', 'GET, POST, HEAD, DELETE, OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'x-token')
// OPTIONS 方法不需要返回任何实体内容,而且应该与最终调用的方法返回的头信息保持一致
})
router.delete('/deleteData', (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', 'http://127.0.0.1:3000')
ctx.set('Access-Control-Allow-Methods', 'GET, POST, HEAD, DELETE, OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'x-token')
// ...
})
前端代码的话,都还是正常写就可以了。不需要加任何额外的参数属性。例子参考 DEMO2。
对于上面的允许的 Headers 头部配置,这个含义是可以支持带有 x-token
的请求,当然你不带上也是可以的。但是你带上了其他字段,比如 x-abc
,那么就不行了。
带 cookie 的请求
上面的两个例子,对于跨域来说,基本上能解决很多问题,比如请求第三方查天气接口,每次请求带上自己的 key 就可以了。跨域处理起来也不算太难。
下面开始说说复杂一点的。
需求是这样的:
-
前后端分离项目,不部署在一起。为了和代码统一,下文用本地环境说明
-
前端域名A (
http://127.0.0.1:3000
),后端接口域名B (http://localhost:3100
) -
前端要先调用登录接口,同时接口会返回状态,并写入
cookie
(其实就是session
) -
前端调用其他接口,需要带上当前的
cookie
(这样后端相当于知道了session
就知道是谁了),才可以获取数据
这次就必须前后端都要修改代码了。