浏览器跨域的原理


跨域问题是前后端开发过程中经常会碰到的问题,那么什么是跨域,为什么前端会出现跨域问题。要了解跨域,先要说说同源策略。同源策略/SOP(Same origin policy)是一种约定,是由 Netscape 公司提出的一个著名的安全策略。它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。当页面在执行一个脚本时会检查访问的资源是否同源,如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。同源策略一般又分为以下两种

  1. OM同源策略:禁止对不同源页面DOM进行操作。主要场景是iframe跨域的情况,不同域名的iframe限制互相访问。
  2. XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。

跨域原理

跨域:指的是从一个域名去请求另外一个域名的资源。即跨域名请求!跨域时,浏览器不能执行其他域名网站的脚本,是由浏览器的同源策略造成的,是浏览器施加的安全限制。跨域的严格一点来说就是只要协议,域名,端口有任何一个的不同,就被当作是跨域。那么如果一个页面调用跨域请求,实际到底发生了什么,请求到底有没有发出去?跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。例如下面的表达 action 中请求百度的地址

<from action="baidu.com">
    // you form filed
</from>

这个表单提交后,剩余的操作就交给了action里面的域 baidu.com,本页面的逻辑和这个表单没啥关系,由于不关系请求的响应,所以浏览器认为是安全的。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。在前端中并不是所有的资源都必须同源,< script >、< img >、< iframe >、< link >这些包含 src 属性的标签可以加载跨域资源。但浏览器限制了JavaScript的权限,所以就算这些标签可以获取跨域资源,但JavaScript却不能读、写加载的其内容。常见的解决方案分为三种:

  1. Nginx代理(前端实现)
  2. JSONP(前端实现)
  3. 后台设置(后端实现)

Nginx就不多说了,通过服务代理的方式可以随意请求到任何其他服务资源,现在主流的前后端分离技术都是通过Nginx实现的,所以现在基本上不会出现跨域问题,下面主要了解一下不使用Nginx的情况下,怎么解决跨域问题。

JSONP方式

JSONP(JSON with Padding)不是一种新技术,而是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。其原理主要是利用< script >标签src属性中的链接可以访问跨域的JS脚本的原理。使用JSONP时需要后端配合,服务端不再返回JSON格式的数据,而是返回一段可执行的JavaScript代码,在src中进行了调用,这样实现了跨域。JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。JSONP实现流程如下

  1. 客户端代码

    <!doctype html>
    <html lang="zh">
        <head>
            <meta charset="utf-8">
            <title></title>    
            <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
            <script type="text/javascript">
                $.ajax({
                    async: false,
                    type : "get",
                    dataType:'jsonp',
                    jsonpCallback:"callback",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
                    url:"http://127.0.0.1:8081/test/jsonp",
                    success:function(data){
                        console.log(data);
                    }
            })
            </script>
        </head>
        <body>
            <div>测试jsonp</div>
        </body>
    </html>
    
  2. 服务端代码

    @RestController
    public class MyController {
    
        @GetMapping("/test/jsonp")
        public void testJsonp(HttpServletRequest request, HttpServletResponse response) throws IOException {
            response.setContentType("text/html;charset=utf-8");
            PrintWriter out = response.getWriter();
            String callback = request.getParameter("callback");
            System.out.println("callback = "+callback);
            JSONObject obj = new JSONObject();
            obj.put("name","test jsonp");
            obj.put("text","测试JSONP");
            out.println(callback+"("+obj.toJSONString()+")");
        }
    }

    需要注意的是服务端返回的数据需要特别的处理一下,上面代码中obj是需要返回的数据,而callback是前端传给我们的前端需要回调的函数名,接口返回给前端的结果如下结构,其中 callback 对应前端的一个可执行函数

    最终前端控制台打印结果如下

刚才的例子是原始的方式去实现后端支持JSONP返回,这样可以让我们只观的知道前端需要什么样的数据以及它的原理。当然对于Spring Boot来说,有更好的实现方式,通过 @ControllerAdvice 增强注解即可

@CrossOrigin注解

使用CrossOrigin注解是后端配置去实现跨域请求,这里虽然指SpringBoot但是SpringMVC也是一样的,要求在Spring4.2及以上的版本,其实现原理也很简单,只是通过这个注解在响应头上加上了允许跨域的报文信息,这样的响应才会被浏览器接受

  1. 客户端代码

    <!doctype html>
    <html lang="zh">
        <head>
            <meta charset="utf-8">
            <title></title>    
            <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
            <script type="text/javascript">
                $.ajax({
                    async: false,
                    type : "get",
                    dataType:'json',
                    url:"http://127.0.0.1:8081/test/json",
                    success:function(data){
                        console.log(data);
                    }
            })
            </script>
        </head>
        <body>
            <div>测试jsonp</div>
        </body>
    </html>
    
  2. 后端代码

    @RestController
    public class MyController {
    
        //实现跨域注解
        //origin="*"代表所有域名都可访问
        //maxAge Cookie的有效期 单位为秒
        //若maxAge是负数,则代表为临时Cookie,不会被持久化,Cookie信息保存在浏览器内存中,浏览器关闭Cookie就消失
        //@CrossOrigin(origins = "*",maxAge = 3600)
        @CrossOrigin
        @GetMapping("/test/json")
        public String testMethod(){
            JSONObject obj = new JSONObject();
            obj.put("name","test json");
            obj.put("text","测试JSON");
            return obj.toJSONString();
        }
    
    }

    关于CrossOrigin注解的参数解释如下

    @Target({ ElementType.METHOD, ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CrossOrigin {
    
        String[] DEFAULT_ORIGINS = { "*" };
    
        String[] DEFAULT_ALLOWED_HEADERS = { "*" };
    
        boolean DEFAULT_ALLOW_CREDENTIALS = true;
    
        long DEFAULT_MAX_AGE = 1800;
        /**
         * 同origins属性一样
         */
        @AliasFor("origins")
        String[] value() default {};
    
        /**
         * 所有支持域的集合,例如"http://domain1.com"。
         * <p>这些值都显示在请求头中的Access-Control-Allow-Origin
         * "*"代表所有域的请求都支持
         * <p>如果没有定义,所有请求的域都支持
         * @see #value
         */
        @AliasFor("value")
        String[] origins() default {};
    
        /**
         * 允许请求头重的header,默认都支持
         */
        String[] allowedHeaders() default {};
    
        /**
         * 响应头中允许访问的header,默认为空
         */
        String[] exposedHeaders() default {};
    
        /**
         * 请求支持的方法,例如"{RequestMethod.GET, RequestMethod.POST}"}。
         * 默认支持RequestMapping中设置的方法
         */
        RequestMethod[] methods() default {};
    
        /**
         * 是否允许cookie随请求发送,使用时必须指定具体的域
         */
        String allowCredentials() default "";
    
        /**
         * 预请求的结果的有效期,默认30分钟
         */
        long maxAge() default -1;
    
    }

对于允许跨域的响应报文主要是 Access-Control-Allow-Origin 起作用,通过刚刚的代码,可以看到请求的响应报文如下

Access-Control-Allow-Origin是标识允许哪个域的请求。如果服务器不通过,响应报文中根本没有这个字段,接着会触发同源策略,再接着你就看到浏览器的提示xxx的服务器没有响应Access-Control-Allow-Origin字段


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
TypeScript学习笔记 TypeScript学习笔记
TypeScript是一种由微软开发的开源 、跨平台的编程语言。它是JavaScript的超集,TypeScript的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言,而且本质上向这个语言添加了可选的静态类型和基于类的
2020-10-08
Next 
Java的发展历史 Java的发展历史
自1946年2月14日世界上首款计算机问世,第一代计算机语言“机器语言”便诞生了,它使用的是最原始的穿孔卡片,这种卡片上使用的语言只有专家才能理解,与人类语言差别极大。这种语言本质上是计算机能识别的唯一语言,人类很难理解。为了能让人们更容易
2020-10-05
  TOC