重学JS | 跨域的原因和解决方案

670 阅读5分钟

这是我参与更文挑战的第16天,活动详情查看:更文挑战

[重学JavaScript系列文章连载中...]

了解跨域之前,我们先熟悉下浏览器同源策略。

浏览器同源策略

同源策略,即为相同协议、域名和端口号,若存在任何一点不同,而为非同源。它是浏览器最基本也是最核心的安全功能,约定客户端脚本在没有明确授权情况下,不能读写不同源的目标资源。主要存在2种表现形式:

  1. DOM同源测试 禁止对不同页面进行dom操作,主要场景是iframe跨域,不同域名下的iframe是会限制访问的。

  2. XMLHttpRequest

禁止使用XMLHttpRequest向不同源的服务器发送ajax请求,防止跨站请求伪造CSRF。

浏览器跨域解决

当没有遵守浏览器的同源策略的时候,会导致跨域限制,很大程度上这也保护了用户隐私数据的安全。但是在某些业务场景中,不可避免的需要进行跨域访问,这怎么解决呢?

1. CORS

CORS全称“ 跨域资源共享”,是HTML5支持的协议,允许浏览器向跨源服务器发起XMLHttpRequest请求。它通过服务器增加特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,则允许XMLHTTPRequest发起跨域请求。这就需要服务端通过对响应头的设置,接收跨域请求处理。这里以java为例配置跨域:

  1. 配置全局过滤器
@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
          config.addAllowedOrigin("*");
          config.setAllowCredentials(true);
          config.addAllowedMethod("*");
          config.addAllowedHeader("*");
          config.addExposedHeader("*");

        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);

        return new CorsFilter(configSource);
    }
}
  1. 配置全局拦截器
@Configuration
public class MyConfiguration extends WebMvcConfigurerAdapter  {

    @Override  
    public void addCorsMappings(CorsRegistry registry) {  
        registry.addMapping("/**")  
           .allowCredentials(true)  
           .allowedHeaders("*")  
           .allowedOrigins("*")  
           .allowedMethods("*");  
    }  
}
  1. 单个请求的跨域通过
@RequestMapping("/hello")
@CrossOrigin("http://localhost:8080") 
public String hello( ){
  return "Hello World";
}

CORS把Http请求分为两类,不同类别按不同的策略进行跨域资源共享协商。

  1. 简单的跨域请求

    1. 请求方法是GET、HEAD或者POST,并且当请求方法是POST时,Content-Type必须是application/x-www-form-urlencoded, multipart/form-data或着text/plain中的一个值。

    2. 请求中没有自定义HTTP头部。

      浏览器要做的就是在HTTP请求中添加Origin Header,将JavaScript脚本所在域填充进去,向其他域的服务器请求资源。服务器端收到一个简单跨域请求后,根据资源权限配置,在响应头中添加Access-Control-Allow-Origin Header。浏览器收到响应后,查看Access-Control-Allow-Origin Header,如果当前域已经得到授权,则将结果返回给JavaScript。否则浏览器忽略此次响应。

  2. 带预检(preflighted)的跨域请求

    1. 除GET、HEAD和POST(only with application/x-www-form-urlencoded, multipart/form-data, text/plain Content-Type)以外的其他HTTP方法。

    2. 请求中出现自定义HTTP头部。

      带预检(Preflighted)的跨域请求需要浏览器在发送真实HTTP请求之前先发送一个OPTIONS的预检请求,检测服务器端是否支持真实请求进行跨域资源访问,真实请求的信息在OPTIONS请求中通过Access-Control-Request-Method Header和Access-Control-Request-Headers Header描述,此外与简单跨域请求一样,浏览器也会添加Origin Header。服务器端接到预检请求后,根据资源权限配置,在响应头中放入Access-Control-Allow-Origin Header、Access-Control-Allow-Methods和Access-Control-Allow-Headers Header,分别表示允许跨域资源请求的域、请求方法和请求头。此外,服务器端还可以加入Access-Control-Max-Age Header,允许浏览器在指定时间内,无需再发送预检请求进行协商,直接用本次协商结果即可。浏览器根据OPTIONS请求返回的结果来决定是否继续发送真实的请求进行跨域资源访问。这个过程对真实请求的调用者来说是透明的。

XMLHttpRequest支持通过withCredentials属性实现在跨域请求携带身份信息(Credential,例如Cookie或者HTTP认证信息)。浏览器将携带Cookie Header的请求发送到服务器端后,如果服务器没有响应Access-Control-Allow-Credentials Header,那么浏览器会忽略掉这次响应。

2. JSOP

JSONP是利用script标签的src属性不受同源策略约束来跨域获取数据的,它实现跨域的特点是简单适用、兼容老浏览器、对服务端影响小。缺点是需要后端配置输出特定的返回信息,并且只支持Get请求,并且回调函数必须是全局的,因为服务端返回后会从全局环境开始找回调函数。

实现步骤分为2步:

  1. 在网页中动态创建script标签,通过script标签向服务器发起请求,在请求中携带一个请求的callback回调函数。
var callback = function(){
  // 回调函数 successFun
  var url = `localhost:8080/getUser?callback=successFun`
  var script = document.createElement('script')
  script.src = url
  document.body.appendChild(script)
}
  1. 在服务器接收到请求后,会处理响应获取返回的参数,然后将参数放在callback回调函数中对应的位置,并将callback回调函数通过json格式进行返回。
// 示意例子,不严谨
@RequestMapping("getUser")
@ResponseBody
public String getUser(String callback){
    User user = new User();
    user.setName("测试测试");
    String str = JSONObject.toJSONString(user);
    //jsonp处理
    return callback + "(" + str + ")";
}

总结,自此我们完成了对跨域问题相关知识的学习。