美文网首页云计算前端开发那些事儿
前端同源策略以及跨域解决方式

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

作者: 鸽屿_ | 来源:发表于2021-01-29 17:54 被阅读0次

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

    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.其它

    相关文章

      网友评论

        本文标题:前端同源策略以及跨域解决方式

        本文链接:https://www.haomeiwen.com/subject/ffyozktx.html