在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容。
??跨越是浏览器进行安全限制的一种方法,如果浏览器禁用了这种安全限制就不会出现跨域问题产生跨域的原因(以下三者都满足):
- 只要调用方访问被调用方的域名、端口、IP不一样
- 浏览器没有禁用安全限制
- 采用了XMLHttpRequest的请求
JavaScript这个安全策略在进行多iframe或多窗口编程、以及Ajax编程时显得尤为重要。根据这个策略,在baidu.com下的页面中包含的JavaScript代码,不能访问在google.com域名下的页面内容;甚至不同的子域名之间的页面也不能通过JavaScript代码互相访问。对于Ajax的影响在于,通过XMLHttpRequest实现的Ajax请求,不能向不同的域提交请求,例如,在abc.example.com下的页面,不能向def.example.com提交Ajax请求,等等。
为什么浏览器要实现同源限制?我们举例说明:
比如一个黑客,他利用iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名和密码登录时,如果没有同源限制,他的页面就可以通过javascript读取到你的表单中输入的内容,这样用户名和密码就轻松到手了。又比如你登录了OSC,同时浏览了恶意网站,如果没有同源限制,该恶意网站就可以构造AJAX请求频繁在OSC发广告帖。
一、跨域问题发生场景
- 特别注意两点:
1、如果是协议和端口造成的跨域问题“前台”是无能为力的
2、在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。比如上面的,http://www.a.com/a.js和http://70.32.92.74/b.js。虽然域名和域名的ip对应,不过还是被认为是跨域。
*“URL的首部”指window.location.protocol +window.location.host。其中,window.location.protocol:指含有URL第一部分的字符串,如http: ,window.location.host:指包含有URL中主机名:端口号部分的字符串.如//www.cenpok.net/server/
二、跨域问题的解决方案
??解决跨域的思路
- 被调用方解决: 被调用方解决-支持跨域(根据http协议关于跨域方面的要求,增加响应头信息,告诉浏览器允许被跨域调用)(因为在发生跨域请求时首先调用方发送一个预检请求(OPTIONS请求),这个请求就会被带上允许跨越的请求头信息)
- 调用方解决:使用代理做调用解决跨域问题-隐藏跨域(利用nginx的反向代理,使访问同一个域名不同的资源路径会代理到不同的服务器上,每个跨域的请求都会带上origin请求头字段,因为访问的资源都是同域名下的,所以不会产生跨越问题)
1、JSONP跨域
? JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。
①原理
我们知道,在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片,像淘宝等大型网站,肯定会将这些静态资源放入cdn中,然后在页面上连接,如下所示,所以它们是可以链接访问到不同源的资源的。
1 <script type="text/javascript" src="某某cdn地址" ></script> 2 <link type="text/css" rel="stylesheet" href="某个cdn地址" /> 3 <img src="某个cdn地址" alt=""/>
而jsonp就是利用了script标签的src属性是没有跨域的限制的,从而达到跨域访问的目的。因此它的最基本原理就是:动态添加一个<script>标签来实现。
②实现方法:
这里是使用ajax来请求的,看起来和ajax没啥区别,其实还是有区别的。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本。
$.ajax({ url:"http://www.baidu.com/service", dataType:‘jsonp‘, data:‘‘, jsonp:‘callback‘, success:function(data) { // some code } });
上面的代码中,callback是必须的,callback是什么值要跟后台拿。获取到的jsonp数据格式如下:
callback({ "code": "CA1998", "price": 1780, "tickets": 5 });
③JSONP的不足之处:
- 只能使用get方法,不能使用post方法:我们知道 script,link, img 等等标签引入外部资源,都是 get 请求的,那么就决定了 jsonp 一定是 get 的。但有时候我们使用的 post 请求也成功,为啥呢?这是因为当我们指定dataType:‘jsonp‘,不论你指定:type:"post" 或者type:"get",其实质上进行的都是 get 请求!
- 没有关于 JSONP 调用的错误处理。如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。
2、跨域资源共享 CORS
? Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
①简单请求
只要同时满足以下两大条件,就属于简单请求。
(1)请求方法是以下三种方法之一:
1 HEAD 2 GET 3 POST
(2)HTTP的头信息不超出以下几种字段:
1 Accept 2 Accept-Language 3 Content-Language 4 Last-Event-ID 5 Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
后端
1 // 处理成功失败返回格式的工具 2 const {successBody} = require(‘../utli‘) 3 class CrossDomain { 4 static async cors (ctx) { 5 const query = ctx.request.query 6 // *时cookie不会在http请求中带上 7 ctx.set(‘Access-Control-Allow-Origin‘, ‘*‘) 8 ctx.cookies.set(‘tokenId‘, ‘2‘) 9 ctx.body = successBody({msg: query.msg}, ‘success‘) 10 } 11 } 12 module.exports = CrossDomain
前端什么也不用干,就是正常发请求就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单请求例子会看到。
1 fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => { 2 console.log(res) 3 })
②非简单请求
非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。
后端
1 // 处理成功失败返回格式的工具 2 const {successBody} = require(‘../utli‘) 3 class CrossDomain { 4 static async cors (ctx) { 5 const query = ctx.request.query 6 // 如果需要http请求中带上cookie,需要前后端都设置credentials,且后端设置指定的origin 7 ctx.set(‘Access-Control-Allow-Origin‘, ‘http://localhost:9099‘) 8 ctx.set(‘Access-Control-Allow-Credentials‘, true) 9 // 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight) 10 // 这种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headers 11 ctx.set(‘Access-Control-Request-Method‘, ‘PUT,POST,GET,DELETE,OPTIONS‘) 12 ctx.set(‘Access-Control-Allow-Headers‘, ‘Origin, X-Requested-With, Content-Type, Accept, t‘) 13 ctx.cookies.set(‘tokenId‘, ‘2‘) 14 15 ctx.body = successBody({msg: query.msg}, ‘success‘) 16 } 17 } 18 module.exports = CrossDomain
一个接口就要写这么多代码,如果想所有接口都统一处理,有什么更优雅的方式呢?见下面的代码
1 const path = require(‘path‘) 2 const Koa = require(‘koa‘) 3 const koaStatic = require(‘koa-static‘) 4 const bodyParser = require(‘koa-bodyparser‘) 5 const router = require(‘./router‘) 6 const cors = require(‘koa2-cors‘) 7 const app = new Koa() 8 const port = 9871 9 app.use(bodyParser()) 10 // 处理静态资源 这里是前端build好之后的目录 11 app.use(koaStatic( 12 path.resolve(__dirname, ‘../dist‘) 13 )) 14 // 处理cors 15 app.use(cors({ 16 origin: function (ctx) { 17 return ‘http://localhost:9099‘ 18 }, 19 credentials: true, 20 allowMethods: [‘GET‘, ‘POST‘, ‘DELETE‘], 21 allowHeaders: [‘t‘, ‘Content-Type‘] 22 })) 23 // 路由 24 app.use(router.routes()).use(router.allowedMethods()) 25 // 监听端口 26 app.listen(9871) 27 console.log(`[demo] start-quick is starting at port ${port}`)
前端
1 fetch(`http://localhost:9871/api/cors?msg=helloCors`, { 2 // 需要带上cookie 3 credentials: ‘include‘, 4 // 这里添加额外的headers来触发非简单请求 5 headers: { 6 ‘t‘: ‘extra headers‘ 7 } 8 }).then(res => { 9 console.log(res) 10 })
3、反向代理
想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了。
Nginx配置
1 server{ 2 # 监听9099端口 3 listen 9099; 4 # 域名是localhost 5 server_name localhost; 6 #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 7 location ^~ /api { 8 proxy_pass http://localhost:9871; 9 } 10 }
前端就不用干什么事情了,除了写接口,也没后端什么事情了
1 // 请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 2 fetch(‘http://localhost:9099/api/iframePost‘, { 3 method: ‘POST‘, 4 headers: { 5 ‘Accept‘: ‘application/json‘, 6 ‘Content-Type‘: ‘application/json‘ 7 }, 8 body: JSON.stringify({ 9 msg: ‘helloIframePost‘ 10 }) 11 })
Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。
原文地址:https://www.cnblogs.com/tag6254/p/10357744.html