Fetch请求的跨域处理

Ajax和Fetch请求的跨域处理

     浏览器安全的基石是"同源政策"(same-origin policy),这里的三源是指协议,端口和域名。同源政策是为了保护用户的安全,如下将介绍,跨域的解决办法。

一. Jsonp
由于这种方式只支持get方法的跨域,本身具有一定的局限性,因此在这里不详细的介绍。
二. Nginx反向代理
如今项目多采用前后端分离的模式,当有前端部署在自己的服务器上,后端也部署在自己的服务器上的情况就会造成前端无法访问后端接口的情况,这时就需要Nginx做一个代理的配置。当a域名访问b域名下某一个接口的时候,先让a域名访问a域名下的某一个url在通过Nginx转发到b域名下的某个接口,这样就让浏览器感觉好像都是在请求同一个服务器下的资源。如下所示为一个配置的例子。
①首先前端的服务器是 admin.swczyc.com 后端的服务器是10.109.252.80,在nginx.conf配置文件中反向代理的配置如下。
在这里插入图片描述
②当在浏览器中访问http://admin.swczyc.com/hongyup/#/user/login连接的时候,由于nginx监听了域名admin.swczyc.com的80端口,当匹配到/hongyup/的时候,通过alias指定了文件路径,该路径下面是前端的工程,因此实际访问的是http://admin.swczyc.com/data/dist/#/user/login。访问到了前端的登录页面。
在这里插入图片描述
③填写用户名和密码之后点击登录,可以看到前端发起的请求信息
在这里插入图片描述
④请求的地址是http://admin.swczyc.com/hyqpi/commen/submit 请求又会被nginx所拦截,并根据如下配置文件进行转发,最后转发到后台所在的服务器http://10.109.252.80/hongyu/coment/submit。
在这里插入图片描述
⑤以上就完成了一次完整的前端到后台的请求,nginx模拟了同源情况下的场景,通过反向代理将前端访问的地址转发到后台真正的服务器上面去。而浏览器此时误以为是同源因此并不会发出跨域的检验。
三. Cors协议
XMLHttpRequest(Ajax底层封装了XMLHttpRequest对象)和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。在这里浏览器发出的请求被分为两类,一类是简单请求,一类是非简单请求。

1.简单请求
当同时满足以下两个条件,就是简单请求

  • 请求方法是HEAD、GET、POST三种之一
  • HTTP的头信息不超出Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type这几个字段,并且Content-Type中只能限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

①一个简单请求 Content-Type:application/x-www-form-urlencoded,Request Method: GET
请求的头信息如图所示
在这里插入图片描述
浏览器在头信息中添加了Origin:http://localhost:8000,服务器返回Access-Control-Allow-Origin响应头,并由浏览器进行验证,看响应头中的Access-Control-Allow-Orign字段是否包含该源,服务器端设置如下,在web.xml中配置一个拦截器拦截所有的请求,并修改响应头。
web.xml配置如下

  <!-- custom cors filter -->
  <filter>
  	<filter-name>CrossDomainFilter</filter-name>
  	<filter-class>com.hongyu.filter.CrossDomainFilter</filter-class>
  </filter>
  <filter-mapping>
  	<filter-name>CrossDomainFilter</filter-name>
  	<url-pattern>/*</url-pattern>
  </filter-mapping>

拦截器修改响应头如下,获取到前台的request头,并将其中的Origin插入到响应头中。

public class CrossDomainFilter extends OncePerRequestFilter {
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
   		  throws ServletException, IOException {
   	String referer = request.getHeader("origin");
   	if (StringUtils.isNotBlank(referer)) {
   	  URL url = new URL(referer);
   	  String origin = url.getProtocol() + "://" + url.getHost();
   	  if(url.getPort()!=-1){
   		  origin+=":"+url.getPort();
   	  }
   	  response.addHeader("Access-Control-Allow-Origin", origin);
   	  response.addHeader("Access-Control-Allow-Credentials", "true");
   	} else {
   	  response.addHeader("Access-Control-Allow-Origin", "*");
   	}
       filterChain.doFilter(request, response);
     }
}

设置成功后,简单请求可以正常访问。
1.非简单请求
非简单请求是那种对服务器有特殊要求的请求,除了上面所说的简单请求,其他都归属于非简单请求,
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。"预检"请求用的请求方法是OPTIONS,服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
下面将列举一些例子:
①当向后台传递Json数据的时候
当前台的数据结构比较复杂,是结构化的数据的时候,需要向后台传递Json类型的数据,前台的数据和请求头如下所示,Content-Type是application/json;charset-utf-8,不属于简单请求中的Content-Type的那三种。因此属于复杂的请求。
在这里插入图片描述
当是发起的复杂的请求的时候,浏览器就会先发一个OPTION请求,进行验证。这时候后台需要再进一步根据复杂请求的验证字段添加Access-Allow-Request-Method和Access-Allow-Request-Headers字段。在原基础上添加如下

public class CrossDomainFilter extends OncePerRequestFilter {
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
   		  throws ServletException, IOException {
   	String referer = request.getHeader("origin");
   	if (StringUtils.isNotBlank(referer)) {
   	  URL url = new URL(referer);
   	  String origin = url.getProtocol() + "://" + url.getHost();
   	  if(url.getPort()!=-1){
   		  origin+=":"+url.getPort();
   	  }
   	  response.addHeader("Access-Control-Allow-Origin", origin);
   	  response.addHeader("Access-Control-Allow-Credentials", "true");
   	} else {
   	  response.addHeader("Access-Control-Allow-Origin", "*");
   	}
       response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
       response.addHeader("Access-Control-Allow-Headers", "Content-Type");
       filterChain.doFilter(request, response);
     }
}

添加之后OPTION预检请求将会返回200,浏览器头信息如下,验证通过之后,才会发一个真正的Post请求。
在这里插入图片描述
②当用antDesign的Upload组件上传图片的时候遇到的跨域问题
当用Upload组件进行上传的时候,跨域情况,会遇到报如下的错误,经过CORS验证,Request header field x-requested-with is not allowed by Access-Control-Allow-Header in preflight response。
在这里插入图片描述
查看了头文件发现在请求头中发现其中request头信息中Access-Control-Request-Header为x-requested-with。而response头中Access-Control-Allow-Header中只包含Content-Type。因此才会报这个Cros验证错误。
在这里插入图片描述
拦截器修改响应头在Access-Control-Allow-Headers字段中,加入x-request-with。

public class CrossDomainFilter extends OncePerRequestFilter {
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
   		  throws ServletException, IOException {
   	String referer = request.getHeader("origin");
   	if (StringUtils.isNotBlank(referer)) {
   	  URL url = new URL(referer);
   	  String origin = url.getProtocol() + "://" + url.getHost();
   	  if(url.getPort()!=-1){
   		  origin+=":"+url.getPort();
   	  }
   	  response.addHeader("Access-Control-Allow-Origin", origin);
   	  response.addHeader("Access-Control-Allow-Credentials", "true");
   	} else {
   	  response.addHeader("Access-Control-Allow-Origin", "*");
   	}
       response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
       response.addHeader("Access-Control-Allow-Headers", "Content-Type,X-Request-With");
       filterChain.doFilter(request, response);
     }
}

虽然这个问题可以通过修改后台的响应头的方式得到解决,但是当自己进行手写fetch方式向后台上传图片的时候却不会出现上面的错误,究其原因就是Access-Control-Request-Header字段为x-requested-with,所以就查看了Upload组件的源码,最后发现是在发起请求的时候在请求头中加了X-Request-With这个字段,来标记这是Ajax请求。因此会出现Header中多设置了一个X-Request-With字段。如下对应源码片

   if (headers['X-Requested-With'] !== null) {
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  }

后来再次查看Upload的官网发现有一个参数可以设置请求头的信息,将X-Request-With设置为null就不会在头中添加该字段了,则该请求就变成简单请求,不会再发OPTION验证了,修改前端代码如下。则后端就不用在Access-Control-Allow-Headers中加X-Request-With 也可以上传成功。

  <Upload
     className={styles.uploadInline}
       name="files"
       onChange={this.handleChange}
       fileList={this.state.fileList}
       beforeUpload={this.handleBeforeUpload}
       headers={{'X-Requested-With' : null}}
     >

四. 跨域遇到的一些其它问题
①fetch请求导出excel读取不到响应头中的content-disposition字段
当前台向后台请求导出excel时,后台会将excel以文件流的方式传递回来,所以需要设置一下request头里面的Accpt为application/vnd.ms-excel,并将后台传回来的文件流转Blob并将 content-disposition被编码的excel名解码出来,用a标签模拟下载动作。fetch函数如下。

export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
    newOptions.headers = {
      Accept: 'application/vnd.ms-excel',
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
      ...newOptions.headers,
    };
  }
  return fetch(url, newOptions)
        .then(response => response.blob().then(blob => {
            var url = window.URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            var filename = response.headers.get('Content-Disposition').split(";")[1].split("=")[1].split(".")[0];
            a.download = decodeURI(filename).concat(".xls");
            a.click(); 
        }))
}

但是发现在跨域的时候,就会获取不到Content-Disposition,但是后台给传回来了,响应头如下,Content-Disposition里面存着编码之后的文件名。
在这里插入图片描述
fetch的回调函数中打印header中的字段,Content-Disposition这个字段并不存在,只有Content-Type存在,在网上查找资料的得知在跨域的情况下前台若想从响应头中获取Content-Disposition,则需要后台在响应头中添加Access-Control-Expose-Headers", "Content-Disposition字段,将这个字段暴露给前台。前台此时在跨域的情况下也能获取到这个字段了。则跨域导出excel就会成功。

以上为前后端跨域连调或部署的时候遇到的问题,欢迎大家讨论交流。

发布了38 篇原创文章 · 获赞 6 · 访问量 8577

猜你喜欢

转载自blog.csdn.net/qq_36400206/article/details/88734136