浏览器之跨域问题

1. 前言

前端道友们在进行前端开发的过程中肯定都遇到过跨域问题,最常见的是不是都是请求后端道友来处理跨域问题呢?反正之前我确实是这么做的,今天想彻底吃透一下关于浏览器跨域的问题,所以写了这篇文章,如有不足之处希望大家能够指正,相互帮助,共同进步。

2. 跨域问题产生的原因

跨域问题其实可以说是浏览器的跨域问题,为什么前面多加了一个“浏览器的”形容词呢?是因为跨域问题只会在浏览器中出现,主要是浏览器的一个安全策略——同源策略导致的。
说到了同源策略,不了解一下什么是同源策略好像说不过去,那就让我们来了解一下什么是同源策略吧!

2.1 同源策略

同源策略是浏览器特有的一种安全策略,简单来说浏览器中访问页面的地址中的协议、域名、端口和浏览器发送的请求地址中的协议、域名和端口完全一致,则说明没有跨域,否则,三者之中有一个不相同,均属于跨域。哪怕不同的域名对应着相同的IP也属于跨域。
在这里插入图片描述

2.2 同源策略的限制

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

在跨域的情况下,浏览器依然会发送请求,并且服务器也能返回响应,只不过结果会被浏览器拦截。

你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

2.3 在跨域下依然可以加在资源的标签

<img src=XXX>
<link href=XXX>
<script src=XXX>

2.4 常见的跨域场景

在这里插入图片描述

3. 跨域问题的解决方法

3.1 CORS

解决跨域问题最简单的方法就是让后端来允许跨域请求啦。

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

同源策略默认禁止跨域请求资源,但是CORS给了web服务器权限,让服务器端可以通过设置允许跨域请求资源。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符(*)则表示所有网站都可以访问资源。

这种方法解决跨域问题,只需要服务端处理就可以了,但是浏览器在发送时会出现两种情况:简单请求复杂请求

3.1.1 跨域涉及到的请求头参数

  1. Access-Control-Allow-Origin
    请求发出去之前,浏览器做了什么?
    它会自动在请求头当中,添加一个Origin字段,用来说明请求来自哪个源。服务器拿到请求之后,在回应时对应地添加Access-Control-Allow-Origin字段,如果Origin不在这个字段的范围中,那么浏览器就会将响应拦截。
    因此,Access-Control-Allow-Origin字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。与此同时,其它一些可选的功能性的字段,用来描述如果不会拦截,这些字段将会发挥各自的作用。

  2. Access-Control-Allow-Credentials
    这个字段是一个布尔值,表示是否允许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而如果需要拿到浏览器的 Cookie,需要添加这个响应头并设为true, 并且在前端也需要设置withCredentials属性:

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
  1. Access-Control-Expose-Headers
    这个字段是给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma), 还能拿到这个字段声明的响应头字段。比如这样设置:
Access-Control-Expose-Headers: aaa

那么在前端可以通过 XMLHttpRequest.getResponseHeader(‘aaa’) 拿到 aaa 这个字段的值。

  1. Access-Control-Allow-Headers
    Access-Control-Allow-Headers

3.1.2 简单请求

满足一下两个条件的请求都是简单请求:

  1. 请求方式:GET、POST、HEAD
  2. 内容类型(content-type):
    multipart/form-data
    text/plain
    application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

3.3.1 复杂请求

不是简单请求的请求都是复杂请求。
浏览器在发送复杂请求之前会先发送一个type:"option"的预请求,验证服务器是否允许跨域请求资源。

发送PUT请求(复杂请求),服务端为了允许跨域请求需要进行如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检请求的有效期,在此期间,不用发出另外一条预检请求
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if ( req.method === 'OPTIONS') {
  res.end()
}
// 定义后台返回的内容
app.put('/getData', function (req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen'

// cookie不能跨域
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')

    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials',true)

    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age',6)

    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers','name')

    if (req.method === 'OPTIONS') {

    // OPTIONS请求不做任何处理
      res.end()
    }

  }
  
  next()
})
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw')

  //返回一个响应头,后台需设置
  res.end('我不爱你')
})

app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

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

3.2 jsonp

jsonp是一种比较传统的解决跨域请求的方式,主要是利用了 script 标签没有跨域问题的特性,使浏览器能够跨域请求json数据。jsonp请求需要服务端支持才行。

3.2.1 JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

3.2.2 JSONP优缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

3.2.3 JSONP的实现流程

  1. 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。

  2. 创建一个 script 标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。

  3. 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show(‘我不爱你’)。

  4. 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。

// index.html
function jsonp ({url, params, callback}) {

  return new Promise((resolve, reject) => {

    let script = document.createElement('script')
    
    window[callback] = function (data){
      resolve(data)
      document.body.removeChild(script)
    }

    params = { ...params, callback }

    // wd=b&callback=show
    let arrs = []

    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}

jsonp({url: 'http://localhost:3000/say', params: { wd: 'Iloveyou'}, callback: 'show'})
  .then(data => { console.log(data) })

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

// server.js
let express = require('express')
let app = express()
app.get('/say', function (req, res) {

  let {wd, callback} = req.query
  console.log(wd)

  // Iloveyou
  console.log(callback)

  // show
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

3.2.4 jQuery的jsonp形式

JSONP都是GET异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。

$.ajax({
    url: "http://crossdomain.com/jsonServerResponse",
	dataType: "jsonp",
	//可以省略
	type: "get",
	//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
	jsonpCallback: "show",
	//->把传递函数名的那个形参callback,可省略
	jsonp: "callback",
	success: function (data){
		console.log(data);
	}
});

3.3 nginx

使用nginx解决跨域问题,是一种十分简单的解决方式,只需要修改nginx配置即可,支持所有浏览器,支持session,不需要修改任何代码,也不会影响服务器性能。
下载nginx,修改nginx目录下的nginx.conf文件:

  1. 正向代理
    正向代理帮助客户端访问客户端自己访问不到的服务器,然后将结果返回给客户端。
    在这里插入图片描述
  2. 反向代理
    反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。
    在这里插入图片描述
    因此,两者的区别就很明显了,正向代理服务器是帮客户端做事情,而反向代理服务器是帮其它的服务器做事情

3.3.1. react项目打包后使用nginx解决跨域问题

1) 修改接口路径:http://127.0.0.1:9080/pkg/….
2) npm run build 打包项目。
3)将打包后的项目放在nginx目录下的html目录下。
4)配置nginx服务器 
server {
   listen  9080;
   server_name  127.0.0.1;
   location /  {
        root   html/build;
        index  index.html index.htm;
   }
   location ^~ /pkg/ {
        proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
   }
}

5)nginx -s reload 启动nginx服务器
6)浏览器输入:http://localhost:9080/ 进入项目
如果项目package.json中配置了homePage属性,要将属性值加入路径中,
例如:homePage: “/web”
览器输入:http://localhost:9080/web/
注释:
a. 配置默认打开文件:

location /  {
   root   html/build;
   index  index.html index.htm;
}

b. 配置反向代理

location ^~ /pkg/ {
   proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
}

3.3.2. react项目在开发模式下解决跨域问题以及cookie问题

1)配置nginx服务器,和上面配置相同,但是要配置请求头
2)修改接口路径:http://127.0.0.1:9080/pkg/….

server {
    listen  9080;
    server_name  127.0.0.1;
    location /  {
         root   html/build;
         index  index.html index.htm;
    }
    location ^~ /pkg/ {
		if ($request_method = 'OPTIONS') {
		   add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
		   add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
		   add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
		   add_header 'Access-Control-Max-Age' 1728000;
		   add_header 'Access-Control-Allow-Credentials' 'true';
		   add_header 'Content-Type' 'text/plain; charset=utf-8';
		   add_header 'Content-Length' 0;
		   return 204;
		}
		
		if ($request_method = 'POST') {
		    add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
		    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
		    add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
		    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
		    add_header 'Access-Control-Allow-Credentials' 'true';
		}
		
		if ($request_method = 'GET') {
		     add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
		     add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
		     add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
		     add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
		     add_header 'Access-Control-Allow-Credentials' 'true';
		}

         proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
    }
}
  1. https://mp.weixin.qq.com/s/xa4fjvfMLjcVbufw3xgjHQ
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值