JavaScript 跨域请求解决方式汇总

目录

什么是跨域?

常见跨域场景

jsonp 跨域

Jsonp - 前端 JavaScript

Jsonp - 服务器

Jsonp - 跨域测试 

Cors 跨域

Cors - 前端 JavaScript 

Cors - 服务器

Cors - 跨域测试

Access-control-Allow-Origin 多域名设置


什么是跨域?

1、前后端数据交互经常会碰到请求跨域,浏览器为了安全使用了“同源策略”

2、同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指"协议+域名+端口"三者全部相同,即便两个不同的域名指向同一个 ip 地址,也非同源。

3、不符合同源策略的请求即为“跨域”。

4、同源策略限制内容有:

1)Cookie、LocalStorage、IndexedDB 等存储性内容

2)DOM 节点

3)AJAX 请求发送后,结果被浏览器拦截了

5、如下三个标签允许跨域加载资源,这也就是平时为什么可以使用 CDN 地址加载网络上的 JQuery、Bootstrap 的原因:

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

6、跨域并不是请求发不出去,而是请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

7、跨域的本质是浏览器为了安全而阻止用户读取另一个域名下的内容,所以表单可以进行跨域请求,因为表单不会获取新的内容,而 Ajax 则会被浏览器拦截结果,因为 Ajax 会获取响应,浏览器认为这不安全,所以拦截了响应。

8、这也说明了跨域并不能完全阻止 CSRF(Cross-site request forgery)跨站请求伪造,因为请求毕竟是发出去了,而且服务端也接收到了请求。

常见跨域场景

1、当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源,就算作“跨域”。

URL 描述 是否跨域

http://www.tiger.com/a.js

http://www.tiger.com/b.js

协议、域名、端口相同

http://www.tiger.com/app1/a.js

http://www.tiger.com/app2/b.js

协议、域名、端口相同,应用不同

http://www.tiger.com:8080/a.js

http://www.tiger.com/b.js

协议、域名相同,端口不同

https://www.tiger.com/a.js

http://www.tiger.com/b.js

协议不同、域名、端口相同

http://www.tiger.com/a.js

http://115.25.183.45/b.js

协议、端口相同,域名不同

http://www.tiger.com/a.js

http://cloudServer.tiger.com/b.js

协议、端口相同,主域名相同,子域名不同

2、浏览器只会通过“URL 首部”来识别而不会根据域名对应的 IP 地址是否相同来判断,“URL 首部”可以理解为“协议, 域名和端口必须匹配”

jsonp 跨域

1、JSONP(JSON with Padding-填充 JSON)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题

2、JSONP 原理:利用 script 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据,JSONP 请求一定需要对方的服务器做支持才可以。

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

4、JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题,缺点是 JSONP 仅支持 get 请求,不支持其它的 POST 等请求,不安全可能会遭受 XSS 攻击。

5、关于纯 JSONP 的实现流程可以参考:https://mp.weixin.qq.com/s/LV7qziMyrMt0_EJWo05qkA

6、本文直接介绍最简单快捷的方式,使用 JQuery 的 $.ajax 类发送 get 请求,指定 jsonp 格式返回。

Jsonp - 前端 JavaScript

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>CSRF跨站请求</title>

    <!-- 引入 JQuery -->
    <script src="https://code.jquery.com/jquery-3.3.1.js"
            integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60="
            crossorigin="anonymous"></script>

    <script type="text/javascript">
        $(function () {
            $("#btn_jsonp").click(function () {
                $.ajax({
                    url: "http://localhost:8080/seals/getInfo", //请求地址
                    type: "GET",    //jsonp只支持get请求,不写时默认为get方式,可省略
                    dataType: "jsonp", //指定返回的数据类型为 jsonp
                    //jsonp:传递给服务器的参数,服务器需要根据此值封装数据返回,不写时JQuery默认为 callback,可省略
                    //这个参数值会自动追加到 Url 地址中 ?callback=xxxx
                    jsonp: "callback",
                    success: function (data) {
                        //返回的jsonp数据必须是 json 格式
                        console.log("success:" + JSON.stringify(data));
                    },
                    error: function (data) {
                        console.log("error:" + JSON.stringify(data));
                    }
                })
            });
        });
    </script>
</head>
<body>
<button id="btn_jsonp" type="button" style="font-size: 20px">jsonp跨域请求</button>
</body>
</html>

1、如上所示不要慌,并不是 JS 代码错了,是因为此时服务器还没开启,顺便提醒一句 $.ajax({jsonp:"callback"}) 的值会自动拼接在 $.ajax({url:"xxx"})地址中,自己可以指定任意的值,当不写 jsonp 属性时,JQuery 默认也是使用 "callback" 作为值。

2、$.ajax({jsonp:"callback"}) 的值服务器端是要获取然后封装值返回的,如 callback({"name":"zhangSan"});后台返回的 json 数据必须放在 callback(xxx) 中。所以跨域请求时,前后端一定要商量好,仅仅只是前端使用了 jsonp ,服务器没有特殊处理时,也是不行的。

Jsonp - 服务器

1、服务器代码如下,需要判断如果是 jsonp 跨域请求,则应该使用 url 地址中的回调参数封装值。

import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * Created by Administrator on 2019/3/13 0013.
 */
@Controller
public class InfoControler {

    /**
     * get请求:http://localhost:8080/seals/getInfo
     *
     * @param request
     * @return
     * @ResponseBody :表示内容直接返回给页面
     */
    @GetMapping("getInfo")
    @ResponseBody
    public String getInfo(HttpServletRequest request) {
        //根据前端$.ajax({jsonp:"callback"}) 设置的参数值进行取值,callback 的值是 JQuery 随机生成的
        String callback = request.getParameter("callback");
        System.out.println("callback=" + callback);

        //返回的参数必须是 json 格式,这里只是为了演示简单,实际中推荐使用 Gson、Fastjson、jackson 等库来操作 json
        String message = "{\"message\":\"I love You 西北的雪\"}";
        if (!StringUtils.isEmpty(callback)) {
            /**
             * callback 不为空,表示此时是 jsonp 跨域请求,后台返回的 json 数据必须放在 request.getParameter("callback") 的参数值中,如 callback(xxx) ,
             * 前端只会取其中的 xxx 纯数据
             * callback 为空时,表示是普通的 get 请求,此时不应该带上 callback(xxx) 中的前缀
             */
            message = callback + "(" + message + ")";
        }
        return message;
    }
}

Jsonp - 跨域测试 

1、服务器应用访问路径为:http://localhost:8080/seals/getInfo

2、js 应用访问路径为:http://localhost:55555/matterJS/html/CSRF.html

3、协议、域名一致,端口不同,这是典型的跨域请求,现在再来从 55555 端口的 js 应用中访问 8080 端口的服务器后台。

4、如上所示,跨域访问完全成功。

Cors 跨域

1、CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),它允许浏览器向跨源(协议 + 域名 + 端口)服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

2、CORS 需要浏览器和后端同时支持,浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域,与前端代码无关。

3、服务器响应客户端的时候,带上 Access-Control-Allow-Origin 头信息则可以开启 CORS, 该属性表示哪些域名可以访问资源,通配符 “*” 表示所有网站都可以访问此资源。

response.setHeader("Access-Control-Allow-Origin", "*");    #允许所有域名的脚本访问该资源
response.setHeader("Access-Control-Allow-Origin", "http://192.168.1.20:55555");  #允许指定的域名的脚本访问该资源

指定某个具体的域名可以访问时,根据同源策略只需要写 协议:/域名:端口 即可,不需要指定应用。

4、如上所示浏览器 F12 开发者工具中随便点击一个请求,就可以看到很多服务器设置的就是允许所有来源的脚本进行访问。

Cors - 前端 JavaScript 

1、上面的 jsonp 跨域只能解决 get 请求,对于其它的 post、put、delete 等请求都是无能无力的,使用 cors 则可以解决一切。

2、前台 javaScript 的代码和平时一样即可,不用任何特殊处理,如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>CSRF跨站请求</title>

    <!-- 引入 JQuery -->
    <script src="https://code.jquery.com/jquery-3.3.1.js"
            integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60="
            crossorigin="anonymous"></script>

    <script type="text/javascript">
        $(function () {
            $("#btn_jsonp").click(function () {
                $.ajax({
                    url: "http://localhost:8080/seals/postInfo", //请求地址
                    type: "POST",    //post 请求
                    data: {"info": "奋六世之余烈8080"},   //传递给服务器的数据
                    success: function (data) {
                        //接收返回的数据
                        console.log("success:" + data);
                    },
                    error: function (data) {
                        //错误时打印错误信息
                        console.log("error:" + JSON.stringify(data));
                    }
                })
            });
        });
    </script>
</head>
<body>
<button id="btn_jsonp" type="button" style="font-size: 30px;margin-top: 50px">Cors跨域请求</button>
</body>
</html>

3、当服务端没有开启“Access-Control-Allow-Origin ”,或是没有为其指定权限时,默认如上所示,无法跨域请求。

Cors - 服务器

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;

/**
 * Created by Administrator on 2019/3/13 0013.
 */
@Controller
public class InfoControler {

    /**
     * post 请求:http://localhost:8080/seals/postInfo
     *
     * @param response
     * @param info
     * @return
     */
    @PostMapping("postInfo")
    @ResponseBody //内容直接返回页面
    public String postInfo(HttpServletResponse response, String info) {
        /**
         * response.setHeader("Access-Control-Allow-Origin", "*"):表示允许所有 域名 的脚本进行访问
         * response.setHeader("Access-Control-Allow-Origin", "http://localhost:55555"):表示允许指定的 协议://域名:端口 中的应用中的脚本访问
         * 注意 协议://域名:端口 必须完全一致,比如 localhost 与 127.0.0.1 虽然都是指向本机,但是也会认为是跨域
         */
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:55555");
        System.out.println("info=" + info);
        return info;
    }
}

1、如上所示此时只允许 http://localhost:55555 中的应用的脚本访问。

Cors - 跨域测试

1、如上所示服务器没有授权的域名是无法访问的。

Access-control-Allow-Origin 多域名设置

1、头信息中的 Access-Control-Allow-Origin 只允许一个值,所以不能用逗号分隔多个值,如下所示是错误的:

Access-Control-Allow-Origin: https://www.google.com,https://www.baidu.com 

2、推荐思路是自己使用 List、Set、Array 等容器存放所有可以访问的域名当做白名单,以后当有请求时动态判断此域名是否在白名单之内,是则放行,否则不处理,客户端也就无法访问成功。

3、这里有个点:如何获取请求的源呢?即用户是从哪个 "协议://域名:端口 "的应用发起访问的呢?可以在浏览器中 F12 打开开发者工具看到请求的头信息中自动带了 Origin 信息,服务器只需要获取即可。

4、下面修改服务端的代码如下,JS代码不变。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by Administrator on 2019/3/13 0013.
 */
@Controller
public class InfoControler {

    /**
     * post 请求:http://localhost:8080/seals/postInfo
     *
     * @param response
     * @param info
     * @return
     */
    @PostMapping("postInfo")
    @ResponseBody //内容直接返回页面
    public String postInfo(String info, HttpServletResponse response, HttpServletRequest request) {
        /**
         * response.setHeader("Access-Control-Allow-Origin", "*"):表示允许所有 域名 的脚本进行访问
         * response.setHeader("Access-Control-Allow-Origin", "http://localhost:55555"):表示允许指定的 协议://域名:端口 中的应用中的脚本访问
         * 注意 协议://域名:端口 必须完全一致,比如 localhost 与 127.0.0.1 虽然都是指向本机,但是也会认为是跨域
         */
        //设置IP 白名单如下,这里只是演示方便才写死,实际中应该从数据库或者配置文件中读取
        Set<String> witchIpSet = new HashSet<>();//
        witchIpSet.add("http://localhost:55555");
        witchIpSet.add("http://192.168.1.20:55555");

        String origin = request.getHeader("Origin");//获取请求头信息中的源

        System.out.println("origin=" + origin);
        if (origin != null && witchIpSet.contains(origin)) {
            System.out.println("白名单之列,放行....");
            response.setHeader("Access-Control-Allow-Origin", origin);
        }
        System.out.println("info=" + info);
        return info;
    }
}

5、此时再次请求时,则只有白名单集合中的源可以正确接收结果,其它的则会受浏览器同源策略的限制而无法接收结果。

6、通常只需要使用 Access-Control-Allow-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')

关于更多跨域解决方案可以参考:https://mp.weixin.qq.com/s/LV7qziMyrMt0_EJWo05qkA

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/88524163