AJAX跨域的前世今生

本文主要参考了如下两篇文章
再也不学AJAX了!(三)跨域获取资源 ① - 同源策略
浏览器同源政策及其规避方法
本文的重点内容是AJAX跨域的来龙去脉,如果想看直接加两行代码就搞定的方法,请直接移步文章末尾。本文主要内容包含:为啥不让跨域?是谁阻止了我的操作?解决的思路是怎样的?同时会涉及到简单请求和复杂请求的跨域解决是不一样的,跨域怎么携带cookie。

1.为什么不让AJAX跨域

在前后端分离的项目中,由于前端和后端不在同一个域,将会导致在发送ajax请求的时候,浏览器给我们报这样一个错:

XMLHttpRequest cannot load http://127.0.0.1:8080/CrossOriginServer/cross. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.

出现这个问题的原因就是浏览器的同源策略:限制不同源之间执行某些特定操作
其中同源要求协议、端口、域名必须完全相同,来看wikipedia给出的示例:

AJAX跨域的前世今生_第1张图片
同源

关于 特定操作有哪些,详细的可以参看开头的第一篇文章。
我也简单总结一下:

  1. 浏览器只会把淘宝给浏览器颁发的cookie发给淘宝,而不会发给百度
  2. 我们无法获取和操作iframe中的dom元素
  3. 限制跨域AJAX请求,这也是我们要说的重点:
1.浏览器的"锅"

首先要明确的是同源策略是浏览器做的限制,也就是说我们之所以前后端分离发送AJAX失败了,全是浏览器的"锅",不关服务器的事。因为Server端只负责提供对外服务,Http请求是无状态的,它不管你是用浏览器还是其他客户端工具发起的请求,只要URL是存在的,就会响应,这点可以从浏览器的F12 Network中看出来,服务端其实已经返回了响应,状态码为200。

2.一切为了安全

那么浏览器为什么要对跨域AJAX请求做限制呢?答案就是为了保障用户数据的安全。以Tomcat为例,session机制绝大多数情况是通过cookie实现的,用户登录A网站后,服务端会为浏览器写入一个cookie,key为JSESSIONID,value为sessionid,用户下次访问该网站的时候,浏览器会自动将该cookie传给A网站,A网站就能识别到这是刚刚登录的那个用户,同时会在返回头中原封不动的把浏览器传上来的cookie再返回去,服务端和浏览器就是这样相互识别的。

假设跨域发送AJAX没有任何限制,那我可以部署一个自己的网站B,对于用户的任何请求,我都额外发一个AJAX到淘宝,如果某个用户的浏览器正好刚访问过淘宝,那么在我发AJAX请求到淘宝时,浏览器会自动将淘宝颁发给用户的cookie带给淘宝,淘宝会在响应中把该cookie原封不动的返给浏览器,那我在AJAX的回调中就能拿到该返回头信息,自然也就能拿到cookie,拿到cookie能干什么事,可以搜索CSRF了解。

2.解决的思路

虽然浏览器的同源策略出发点是好的,为了保障用户信息安全,但是前后端分离这种合法场景也不幸躺枪,我们既要遵循同源策略,又要实现跨域,该怎么办?
首先基于上面的分析,我们可以看出,浏览器之所以阻止了AJAX的响应,是因为浏览器担心A服务器在不知情的情况下把用户信息携带给客户端。所以问题的核心在于打消浏览器的顾虑,这就需要A来配合,告诉浏览器,你别担心,都是自己人,这样做是合法安全的,你不要再拦截响应了。于是你就会发现网络搜索的各种文章让你在服务端加入这句话:

resp.addHeader("Access-Control-Allow-Origin", "*");

就是用来告诉浏览器,第二个参数指定的域发起的请求是合法的,我们商量好的,你放心,不要拦截响应。通过这句话,我们AJAX发送的Get Post请求都能正常响应了。

3.Put Delete请求

但如果你用了Rest风格的API,你会发现在发Put Delete请求时,还是会报错,Put方法根本没进去,F12发现浏览器发送了一个OPTIONS请求,没发我们的Put请求。
这就是因为本次跨域是一个复杂请求,需要先发OPTIONS问问服务器,你支持哪些请求,如果服务器支持我要发的Put请求,才会真正发送。那什么是简单请求和复杂请求呢?
简单请求:

  • 请求方法只属于HEAD,GET,POST请求的其中一种
  • HTTP的头信息只限于以下字段:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
  • Content-Type只能为:application/x-www-form-urlencoded,multipart/form-data,text/plain其中一种
    不满足如上条件的就是复杂请求。

以上面的Put请求为例,以Servlet为例,我们需要重写doOptions方法,然后指明服务器允许哪些域,允许什么请求,允许哪些头等等。注意所有的这些指定都是针对跨域有效的,在同域中调用会完全忽略这些头的存在。

@Override
protected void doOptions(HttpServletRequest arg0, HttpServletResponse resp) throws ServletException, IOException {
    resp.addHeader("Access-Control-Allow-Origin", "*");
    resp.addHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
    resp.addHeader("Access-Control-Allow-Headers", "Any-Name-Here");
}

然后在doPut()方法中,也要指明允许的域,然后就可以写正常的业务逻辑了。

@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.addHeader("Access-Control-Allow-Origin", "*");
    resp.getWriter().write("这里就可以写你的业务逻辑了");
}

4.携带cookie信息

跨域AJAX默认是不会携带cookie信息的,如果想带cookie,就需要服务端和浏览器协作了。
浏览器端需要指明携带cookie:

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

后端需要指定明确的域,指明允许携带验证信息

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这里写*,好像也能接收跨域的cookie
        resp.addHeader("Access-Control-Allow-Origin", "*");//"http://localhost:8080");
        //这句话好像也可以不要
        resp.addHeader("Access-Control-Allow-Credential", "true");
        if(req.getSession().getAttribute("user") != null){
            resp.getWriter().write("合法用户才能看到的信息");
        }
    }

在参考文章中说后端也需要做对应处理,指明具体的域,不能用*,要加一个Access-Control-Allow-Credential头,但我试了好像只要在Ajax指明withCredentials,就可以了,大家有兴趣可以试一下。

5.总结

当然我们用的可能是$.ajax()和springMVC,为了解决跨域,我们可以这样做:
前端

//发登录请求
$.ajax({
        type: "post",
        url: "http://127.0.0.1:8080/CrossOriginServer/login",
        data: {param:"跨域登录"},
        xhrFields: {
            withCredentials: true
         },
        success: function (data, status) {
            console.log(data);
        }
    });
//其他请求 put delete等等
$.ajax({
        type: "put",
        url: "http://127.0.0.1:8080/CrossOriginServer/cross",
        data: {param:"跨域携带cookie测试"},
        xhrFields: {
            withCredentials: true
         },
        success: function (data, status) {
            console.log(data);
        }
    });

后端

springmvc-servlet.xml 增加如下配置

    

springmvc的官方文档对跨域的方案有详细的说明,如果遇到问题还是最好根据自己用的版本先去官方文档找找,相比网上搜的资料,既清楚又权威。

你可能感兴趣的:(AJAX跨域的前世今生)