同源策略与跨域

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

不论个人练习还是实际开发,我们都不可避免地会遇到跨域问题,而造成跨域的罪魁祸首就是浏览器的同源策略。要解决跨域,首先要了解同源策略。

主要内容

  • 同源策略与跨域
  • 解决跨域
    • JSONP
    • CORS

同源策略与跨域

同源策略

百度“同源策略”得到以下回答:

同源策略,它是由Netscape提出的一个著名的安全策略。所有支持JavaScript 的浏览器都会使用这个策略。

所谓同源是指,域名,协议,端口相同。

在一个浏览器的两个tab页中分别打开百度和谷歌的页面,当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

同源策略是浏览器的行为,是浏览器为了保护本地数据不被JavaScript代码获取回来的数据污染而做出的拦截行为。即请求发送了,服务器响应了,但是无法被浏览器接收。

简单来说,就是:

同源策略与跨域_第1张图片

很多人以为同源策略是浏览器不让请求发出去、或者后端拒绝返回数据。NO!实际情况是,请求正常发出,后端接口也正常响应,只不过数据到了浏览器后被丢弃了。


同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM节点
  • AJAX跨域请求的数据

以下情况都属于跨域:

跨域原因说明

示例

域名不同

www.jd.com

www.taobao.com

域名相同,端口不同

www.jd.com:8080

www.jd.com:8081

二级域名不同

item.jd.com

miaosha.jd.com

协议不同

HTTP与HTTPS

简单来说,是否跨域的3个因素为:协议、域名、端口。

需要注意,如果协议、域名、端口都相同,但是请求路径不同,不属于跨域,如:

https://www.jd.com/item

https://www.jd.com/goods

而上面示意图中,在manage.leyou.com的页面访问api.leyou.com的接口,由于二级域名不同,导致跨域。从这个角度来看,只要是前后端分离的项目,必然跨域!(即使部署在同一个服务器,前后端项目端口肯定不同)

跨域Demo

随手建一个SpringBoot项目后,把下面的文件拷过去

目录结构

同源策略与跨域_第2张图片

avatar.png是头像,你们随便用啥。

UserController

@RestController
public class UserController {

    @GetMapping(value = "/getUser/{id}")
    public User getUser(@PathVariable("id") Long id) {
        // id没用上,就是演示一下@PathVariable注解
        System.out.println("id:" + id);

        User user = new User();
        user.setName("mx");
        user.setAge(18);
        user.setAddress("wenzhou");

        return user;
    }
}

index.htm




    
    CORS
    


当前网页来自localhost:7070/index.html

页面加载时自动发送GET请求: http://localhost:8080/avatar.png



点击发送GET请求: http://localhost:8080/getUser/1

JQuery你们可以引用外站的:

刚才提到过,协议、端口、域名都能造成跨域,这里我们演示最简单的跨域:端口不同导致跨域,我会把同一个项目启动两次。

第一次通过IDEA启动7070端口:

同源策略与跨域_第3张图片 

第二次通过java -jar指定8080端口启动应用(或者可以再配置一个IDEA的启动按钮,更改yml端口后启动即可):

# install
mvn clean install -Dmaven.test.skip=true
# 指定8080端口启动项目
java -jar /Users/kevin/IdeaProjects/springboot-demo/target/springboot-demo-0.0.1-SNAPSHOT.jar --server.port=8080

同源策略与跨域_第4张图片

请求端口为7070的应用,得到index.html页面,随后点击按钮,向端口为8080的应用发起请求:

同源策略与跨域_第5张图片

index.html来自端口为7070的应用,它总共向端口为8080的应用发送两次请求:

  • 标签自动发起请求,获取头像成功
  • 点击botton发起请求,获取User失败

但botton的GET请求真的失败了吗?其实AJAX已经拿到了数据(状态码200),只不过浏览器拒绝该数据。

同源策略与跨域_第6张图片

回顾一下开头的那张示意图:

同源策略与跨域_第7张图片

本案例中,页面index.html来自localhost:7070,而和AJAX的GET都指向localhost:8080,都跨域了:

  • $.get('http://localhost:8080/getUser/1', function (data){...})

但为什么只有AJAX被拒绝了?

因为浏览器允许跨域。

是的,浏览器遵守同源策略,但是有若干个标签是允许跨域的,比如:

  • 其次,

    当前网页来自localhost:7070/index.html

    页面加载时自动发送GET请求: http://localhost:8080/avatar.png


    Controller

    @GetMapping(value = "/getUser/{id}")
    public String getUser(@PathVariable("id") String id) {
    
        System.out.println("id:" + id);
    
        return "printResponse" + "(" + "{\n" +
                "  \"username\": \"mx\",\n" +
                "  \"age\": 18,\n" +
                "  \"address\": \"China\"\n" +
                "}" + ")";
    
    }

    既然

    • 利用

      当前网页来自localhost:7070/index.html

      页面加载时自动发送GET请求: http://localhost:8080/avatar.png


      @RestController
      public class UserController {
      
          @GetMapping(value = "/getUser/{id}")
          public String getUser(@PathVariable("id") String id, String callback) {
      
              System.out.println("id:" + id);
              // 拼接方法名
              return callback + "(" + "{\n" +
                      "  \"username\": \"mx\",\n" +
                      "  \"age\": 18,\n" +
                      "  \"address\": \"China\"\n" +
                      "}" + ")";
      
          }
      }

      第三版:JQuery的JSONP

      经过前两版的演变,大家其实已经知道JSONP的大致原理了,来看看JQuery的JSONP处理方式是怎样的吧~

      
      
      
          
          CORS
          
      
      
      

      当前网页来自localhost:7070/index.html

      页面加载时自动发送GET请求: http://localhost:8080/avatar.png



      点击发送GET请求: http://localhost:8080/getUser/1

      @RestController
      public class UserController {
      
          /**
           * SpringBoot内置的Jackson
           */
          @Autowired
          private ObjectMapper objectMapper;
      
      
          @GetMapping(value = "/getUser/{id}")
          public String getUser(@PathVariable("id") String id, String callback) {
      
              System.out.println("id:" + id);
      
              User user = new User();
              user.setName("mx");
              user.setAge(18);
              user.setAddress("China");
      
              // 拼接方法名
              try {
                  return callback + "(" + objectMapper.writeValueAsString(user) +")";
              } catch (JsonProcessingException e) {
                  e.printStackTrace();
              }
      
              return null;
      
          }
      }

      同源策略与跨域_第11张图片

      可以看到,JQuery自动帮我们在URL里加了callback参数。复制URL直接用浏览器请求:

      大致流程是:前端随机生成不可见的方法xxx -> 传递到后端,后端拼接JS脚本xxx(User) -> 前端得到JS脚本调用xxx(User) -> 最终调用到success:function(data){...}

      随机生成的xxx方法其实就是一个代理而已。

      CORS

      JSONP的缺点其实挺多的,百度一下就能了解到。最显著的缺点其实是JSONP只支持GET请求,毕竟它的灵感来自于

      当前网页来自localhost:7070/index.html

      页面加载时自动发送GET请求: http://localhost:8080/avatar.png



      点击发送GET请求: http://localhost:8080/getUser/1

      /**
       * @author mx
       */
      @RestController
      public class UserController {
      
          /**
           * 在跨域方法上加@CrossOrigin即可完美解决跨域问题
           * @param id
           * @return
           */
          @CrossOrigin("http://localhost:7070")
          @GetMapping(value = "/getUser/{id}")
          public User getUser(@PathVariable("id") String id) {
      
              System.out.println("id:" + id);
      
              User user = new User();
              user.setName("bravo");
              user.setAge(18);
              user.setAddress("China");
      
              return user;
      
          }
      }

      同源策略与跨域_第13张图片

      Controller上加@CrossOrigin

      @CrossOrigin还可以加载Controller上,这样Controller的所有方法都支持跨域。

      @RestController
      @CrossOrigin("http://localhost:7070")
      public class UserController {
      
          @GetMapping(value = "/getUser/{id}")
          public User getUser(@PathVariable("id") String id) {
      
              System.out.println("id:" + id);
      
              User user = new User();
              user.setName("bravo");
              user.setAge(18);
              user.setAddress("China");
      
              return user;
      
          }
      }

      @Bean配置跨域Filter

      /**
       * @author mx
       */
      @Configuration
      public class CorsConfig {
          @Bean
          public CorsFilter corsFilter() {
              //1.添加CORS配置信息
              CorsConfiguration config = new CorsConfiguration();
              //1) 允许的域,不要写*,否则cookie就无法使用了
              config.addAllowedOrigin("http://localhost:7070");
              //2) 是否发送Cookie信息
              config.setAllowCredentials(true);
              //3) 允许的请求方式
              config.addAllowedMethod("*");
              // 4)允许的头信息
              config.addAllowedHeader("*");
              // 5) 有效时长
              config.setMaxAge(3600L);
      
              //2.添加映射路径,我们拦截一切请求
              UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
              configSource.registerCorsConfiguration("/**", config);
      
              //3.返回新的CorsFilter.
              return new CorsFilter(configSource);
          }
      }

      WebMvcConfigurer添加跨域规则

      /**
       * @author mx
       */
      @Configuration
      public class CorsConfig implements WebMvcConfigurer {
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/**")
                      .allowedOrigins("http://localhost:7070")
                      .allowCredentials(true)
                      .allowedHeaders("*")
                      .allowedMethods("*")
                      .maxAge(3600L);
          }
      }

      以上三种方式任选一种皆可,推荐最后两种。你会发现,在上面几种方式的演进中,跨域规则被一步步抽取到“更高层次”的位置。如果你们公司恰好使用微服务,还可以把跨域规则提取到网关,也就是把Filter移到网关对所有微服务做统一跨域处理。

      CORS原理

      回顾之前跨域的报错信息:

      同源策略与跨域_第14张图片

      浏览器认为只要后端没返回CORS头(Access-Control-Allow-Origin),就认为后端不允许跨域,返回的数据不可靠。

      同源策略与跨域_第15张图片

      所以只要后端能够返回浏览器需要的请求头,即可跨域(响应数据就不会被同源策略抛弃):

      上面是表面原理,底层原理比较复杂。

      浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。

      简单请求

      只要同时满足以下两大条件,就属于简单请求。:

      (1) 请求方法是以下三种方法之一:

      • HEAD
      • GET
      • POST

      (2)HTTP的头信息不超出以下几种字段:

      • Accept
      • Accept-Language
      • Content-Language
      • Last-Event-ID
      • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

      当浏览器发现发起的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.

      刚才说过,CORS需要客户端和服务端同时支持。上面这个小操作,算是客户端的支持行为(IE10以下不行)。

      同源策略与跨域_第16张图片

      Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

      如果服务器允许跨域,需要在返回的响应头中携带下面信息(算是服务端的支持):

      Access-Control-Allow-Origin: http://manage.leyou.com
      Access-Control-Allow-Credentials: true
      Content-Type: text/html; charset=utf-8

      • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*(代表任意域名)
      • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

      有关cookie:

      要想操作cookie,需要满足3个条件:

      • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
      • 浏览器发起ajax需要指定withCredentials 为true
      • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

      这样一来,前后端都支持跨域了,那就跨吧。

      特殊请求

      不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。

      预检请求

      特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

      浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

      一个“预检”请求的样板:

      OPTIONS /cors HTTP/1.1
      Origin: http://manage.leyou.com
      Access-Control-Request-Method: PUT
      Access-Control-Request-Headers: X-Custom-Header
      Host: api.leyou.com
      Accept-Language: en-US
      Connection: keep-alive
      User-Agent: Mozilla/5.0...

      与简单请求相比,除了Origin以外,多了两个头:

      • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
      • Access-Control-Request-Headers:会额外用到的头信息

      预检请求的响应

      服务的收到预检请求,如果许可跨域,会发出响应:

      HTTP/1.1 200 OK
      Date: Mon, 01 Dec 2008 01:15:39 GMT
      Server: Apache/2.0.61 (Unix)
      Access-Control-Allow-Origin: http://manage.leyou.com
      Access-Control-Allow-Credentials: true
      Access-Control-Allow-Methods: GET, POST, PUT
      Access-Control-Allow-Headers: X-Custom-Header
      Access-Control-Max-Age: 1728000
      Content-Type: text/html; charset=utf-8
      Content-Encoding: gzip
      Content-Length: 0
      Keep-Alive: timeout=2, max=100
      Connection: Keep-Alive
      Content-Type: text/plain

      除了Access-Control-Allow-OriginAccess-Control-Allow-Credentials以外,这里又额外多出3个头:

      • Access-Control-Allow-Methods:允许访问的方式
      • Access-Control-Allow-Headers:允许携带的头
      • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

      如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

      另外,需要注意CSRF攻击,也就是跨域伪造请求。即使有同源策略存在,如果不做另外防护,此类攻击仍然奏效。具体大家另外了解。

      作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

      同源策略与跨域_第17张图片进群,大家一起学习,一起进步,一起对抗互联网寒冬

你可能感兴趣的:(javaWeb进阶,javaweb)