前端同源策略以及跨域解决方式

一、同源:域名、端口、协议都相同。如果有一个不同,则是跨域。

url解析图

同源策略是浏览器的安全策略,用来阻止origin文档或者是它加载的脚本与另外一个源的资源进行交互。可以阻挡恶意文档,减少被攻击的路径。
同源策略是一种约定,web是构建在此基础之上的,浏览器只是真的就它的一种实现。
当在浏览器打开两个tab页,分别是百度谷歌,当在百度加载脚本时,将会检查这个脚本是否同源,如果不是,则会拒绝访问。
同源策略是浏览器的行为,为了保护本地数据不被请求回来的数据污染,拦截的是客户端发出的请求请求回来的数据,服务器响应了数据,但是被同源策略拦截。

二、跨域:非同源资源之间尝试通信,将产生跨域

同源策略出于安全考虑,限制了以下行为:Cookie、LocalStorage、IndexDB无法读取,DOM和JS对象无法获取、Ajax请求发送不出去
但是有三个标签允许跨域加载资源:<img src=xxx><link href><script src=xxx>

触发跨域:非同源请求,服务端设置cors限制。


常见跨域场景

特别注意:
第一:如果是协议和端口造成的跨域问题,前台是无法解决的。
第二:在跨域问题上,仅仅是通过"URL的首部"来识别而不会根据域名对应的IP地址是否相同来判断,"URL首部"可以理解为协议、域名、端口。
第三:跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。表单方式可以发起跨域请求是因为它不会获取新的内容,所以可以发送请求,这也说明了跨域并不能完全阻止CSRF,因为阻止的只是响应消息。

三、解决跨域方式

1.通过jsonp跨域

-原理:利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的JSON数据。JSONP请求一定需要对方的服务器做支持才可以。
-JSONP和AJAX对比:JSONP和AJAX相同,都是客户端向服务端发送请求,从服务器端获取数据的方式。但是AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
-JSONP优缺点:JSONP的有点是简单,兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
-JSONP的实现流程:
1.声明一个回调函数(show),其函数名当作参数值,传递给跨域请求数据的服务器,函数形参为要获取的目标数据(服务器返回的data)。
2.创建一个<script>标签,把跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递第一步创建好的回调函数函数名(可以通过问号传参?callback=show),服务器接收到请求后,需要进行特殊的处理,把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如传递进来的函数名是show,它准备好的数据是show('我不爱你')。
3.最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
在开发钟可能会遇到多个JSONP请求的回调函数名是相同的,这时候就需要自己封装一个JSONP函数。

function jsonp({url,params,callback}){  //三个参数,地址,参数,回调函数名
  return new Promise((resolve,reject) =>{//返回一个promise对象,异步执行下一步
    let script = document.createElement('script')   //获取script的dom
    window[callback] = function(data){  //给window添加用了一个callback方法
      resolve(data)
      document.body.removeChild(script)  //移除创建的标签
    }
      params = { ...params , callback }  //将params和callback合并
      let arrs = []
      for(let key in params){
        arrs.push(`${key}=${params[key]}`)  //遍历对象,依次放入数组得到wd=b&callback=show的数据
      }
       script.src = `${url}?${arrs.join('&')}`//拼接url和参数
        document.body.appendChild(script)  //往dom添加script标签
  })
}
jsonp({
    url:'http://localhost:3000/say',
    params:{wd:'I love you'},
    callback:'show'
}).then(data=>{
      console.log(data)
})

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出我不爱你。

//serve.js
let express = require('express')
let app = express().Route
app.get('/say',function(req,res,next)=>{
  let {wd,callback} = req.query
  console.log(wd)
  console.log(callback)
  res.send(`${callback}('我不爱你')`)
})
app,listen(3000)

总结:使用jsonp方法,前后端都需要操作,前端需要通过script标签,进行跨域请求,首先把url,params,callback进行处理,得到一长串的url+参数+回调函数,再把它放到script的src中,后端接收到请求后,首先解构赋值得到参数以及回调函数名,接着返回函数名+data的字符串拼接,接着前端因为是一个promise对象,所以会接着执行给callback函数,resolve去打印了data,同时删除了script标签。
-jQuery的jsonp形式
JSONP都是GET和异步请求的,不存在其它的请求方式和同步请求,且jQuery默认就会给JSONP的请求清楚缓存。

$.ajax({
url:'http://myapp.com/jsonServerResponse',
dataType:'jsonp',
type:'get',  //可以省略
jsonCallback:'show',   //自定义传送给服务器的函数名,而不是使用jQuery自动生成的,可忽略
jsonp:'callback',  //把传递函数名的形参设定,可忽略。
sucess:function(data){
console.log(data)
}
})

2.CORS解决跨域

cors需要浏览器和后端同时支持。IE8和IE9需要通过XDomainRequest来实现。
浏览器会自动进行CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,就实现了跨域。
服务端设置Access-Control-Allow-Origin就可以开启CORS。该属性表示哪些域名可以访问资源,如果设置了通配符,那么所有网站都可以访问资源。
虽然设置CORS和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别是简单请求和复杂请求。
(1)简单请求
只要同时满足以下两大条件,就属于简单请求
条件1:使用下列方法之一:GET/HEAD/POST
条件2:Content-Type的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-from-urlencoded
请求中的任意XML对象均没有注册任何事件监听器;XML对象可以使用XML.HttpRequest.upload属性访问。
(2)复杂请求
不符合简单请求规则的便是复杂请求,复杂请求的CORS设置,会在正式通信之前,增加一次HTTP查询请求,称为预检请求,该请求是option方法的,通过该请求来知道服务端是否允许跨域请求。
我们用PUT向后台请求时,就属于复杂请求,后台需做如下配置:

res.setHeader('Access-Control-Allow-Methods','PUT')  //允许访问的方法
res.setHeader('Access-Control-Max-Age',)   //预检的存活时间
if(req.method === 'OPTIONS' ){
res.end()
}
app.put('/getData',function(req,res){
  console.log(req.headers)
  res.end('i don not love you')
})

接下来看一个完整例子以及CORS请求的相关字段。

//index.html
let xhr = new XMLHttpRequest()  //new一个XML对象
document.cookie = 'name = xiamen'  //cookies不能跨域
xhr.withCredentials = true  //前端设置是否可以携带cookie
xhr.open('PUT','http://localhost:4000/getData',true)
xhr.setRequestHeader('name','xiamen') //设置请求头
xhr.onreadystatechange = function (){
    if(xhr.readyState === 4){
        if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
            console.log(xhr.response)
            //得到响应头,后台需要设置Access-Control-Expose-Headers
            console.log(xhr.getResponseHeader('name'))
      }
  }
}
xhr.send()

//server1.js
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.listen(3000)
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000']  //设置白名单
app.use(function(req,res,next){
  let origin = req.headers.origin
  if(whitList.includes(origin)){
      res.setHeader('Access-Control-Allow-Origin',origin)  //允许哪个源访问
      res.setHeader('Access-Control-Allow-Headers','name') //允许哪个头可以访问
      res.setHeader('Access-Control-Allow-Methods','PUT') //允许的方法
      res.setHeader('Access-Control-Allow-Credentials',true)//允许携带cookie
      res.setHeader('Access-Control-Max-Age',6) //预检的存活时间
      res.setHeader('Access-Control-Expose-Headers','name') //允许返回的头
      if(res.method === 'OPTIONS'){
          res.end() //OPTIONS请求不做任何处理
      }
  }
      next()
})
app.put('/getData',function(req,res){
    console.log(req.headers)
    res.setHeader('name','jw') //返回一个响应头,后台需要设置
    res.send('啦啦啦')
})
app.get('/getData',function(req,res){
    console.log(req.headers)
    res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

上述代码由http:localhost:3000/index.htmlhttp://localhost:4000/跨域请求,正如我们上面说的,后端是实现CORS通信的关键。

补充

cors跨域解决方式:简单请求时,浏览器会直接发出CORS请求(在头信息之中,增加一个origin字段)。origin字段('Origin: http://api.bob.com')用来说明本次请求来自哪个源(协议+端口+域名),服务器根据这个值,决定是否同意这次请求。如果后端接收到的Origin指定的源,不在许可的范围,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,于是抛出错误,被XML的onerror函数捕获。注意的是,这种错误无法通过状态码识别,因为HTTP回应的状态码有额能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin:http://api.bob.com  
//必须的,是请求时的origin字段的值或是*,表示接收任何域名的请求
Access-Control-Allow-Credentials:true
//是否浏览器允许发送Cookie
Access-Control-Expose-Headersw:FooBar
//可选,CORS请求时,XML对象的getResponseHeader()方法只能拿到6个基本字段:`Cache-Control`、`Content-Language`、`Content-Type`、`Last-Modified`、`Pragma`、`Expires`。如果想拿到其他字段,就必须在Acess-Control-Expose-Headers里面指定、上面例子中指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
Content-Type:text/html;charset=utf-8

withCredentials:CORS请求默认不发送cookie和HTTP认证信息,如果要把Cookie发到服务器,一方面需要服务器同意,指定Access-Control-Allow-Credentials字段为true,另外一方面,开发者必须在AJAX请求中打开withCredentials属性

var xhr = new XMLHttpRequest()
xhr.withCredentials = true

所以需要两端一个配合才能实现cookie的传送,但是有时候,就算我们没有在前端设置这个值,浏览器也会发送,我们可以将其设置为false进行关闭。
如果需要发送cookie,Access-Control-Allow-Origin就不能设置为*号,必须指定明确,与网页请求一直的域名,cookie依然遵循同源策略,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传。
复杂请求(非简单请求)
比如请求方法是PUT或者是DELETE,或者Content-Type的字段是application/json等对服务器由特殊要求的请求。
在正式通信前,增加一次HTTP查询请求,成为预检请求。
预检请求中,浏览器先询问服务器,当前网页的域名是否在服务器的许可名单中,以及可以使用哪些HTTP动词和头信息字段,只有得到肯定答复,浏览器才会发出正式的XML请求,否则就会报错。
我们来看一段浏览器的js脚本

var url = 'http://api.alice.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('PUT',url,true)    //第三个参数是是否异步
xhr.setRequestHeader('X-Custom-Header','value')
xhr.send()

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header,浏览器发现,这不是一个简单的请求,就自动发出一个‘预检’请求,要求服务器确认可以这样请求,下面是这个预检的请求头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com     //域名信息
Access-Control-Request-Method: PUT   //请求方法
Access-Control-Request-Headers: X-Custom-Header   //请求头钟的特殊字段
Host: api.alice.com  
Accept-Language: en-US  //接收语言
Connection: keep-alive  
User-Agent: Mozilla/5.0...

预检请求用的方法是OPTIONS,表示这个请求是用来询问的,头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,‘预检’请求的头信息还包含两个特殊的字段。
(1)Access-Control-Request-Method
必须的方法,列举浏览器会用到哪些方法
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
预检请求的回应
服务器收到预检请求后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段后,确认允许跨域请求,就可以做出回应

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT   //时间
Server: Apache/2.0.61 (Unix)  //服务器
Access-Control-Allow-Origin: http://api.bob.com  //允许跨域的域名
Access-Control-Allow-Methods: GET, POST, PUT  //允许的方法
Access-Control-Allow-Headers: X-Custom-Header  //允许的额外头信息
Content-Type: text/html; charset=utf-8  //内容格式
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

如果服务器否定了预检请求,会返回一个正常的HTTP回应,但是灭有任何CORS相关的头信息字段。这是,浏览器就会认定,服务器不同意预检请求,因此触发错误,被XML的onerror捕获,将会报错如下信息

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

当请求通过预检之后,请求将和简单请求一样,通过Access-Control-Allow-origin判断浏览器发送过来的请求是否允许,允许,则做出正常的回应。
CORS与JSONP的比较:
CORS和JSONP的使用目的相同,但是比jsonp更加强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老浏览器,以及可以向不支持CORS的网站发请求。
3.iframe、hash

4.CORS(Cross-Origin-Resource-Sharing)

5.服务器跨域,服务器中转代理

6.其它

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容