浏览器跨域问题总结(Cross-Origin)

1. 背景

首先解释一下同源策略: 同源策略,是由 Netscape 提出的一个著名的安全策略。现在所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,子域名,协议,端口都相同。

早前浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略。但是 HTML 的 <script> 标签是例外,可以突破同源策略从其他来源获取数据。利用这个特性,一些人想出了很 trick 的方式:JSONP。

但是,在当今的 Web 开发中,使用跨域 HTTP 请求加载各类资源(包括 JSON、CSS、图片、JavaScript 脚本以及其它类资源),已经成为了一种十分普遍的方式。所以 W3C 推荐了一种新的机制,即跨源资源共享(Cross-Origin Resource Sharing (CORS))。这种机制让 Web 应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。要使 XMLHttpRequest 在现代浏览器中可以发起跨域请求:

  • 浏览器必须能支持跨源共享带来的新的组件,包括请求头和策略执行。
  • 服务器端则需要解析这些新的请求头,并按照策略返回相应的响应头以及所请求的资源。

好在现代浏览器都已经支持 CORS 了,我们只需要在服务端处理跨域请求就可以。

结论:推荐使用 CORS

2. jsonp

JSONP(JSON with Padding),并非新的数据格式,而是解决JSON跨域获取的解决方案。由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON 数据。它主要利用了 <script/> 标签对 javascript 文档的动态解析。

实现步骤:

  1. 首先在客户端注册一个 callback , 然后把 callback 的名字传给服务器。
  2. 服务器先生成 JSON 数据。然后以 JavaScript 语法的方式,生成一个 function , function 名字就是传递上来的参数 callback。然后,将 JSON 数据直接以入参的方式,放置到 function 中,这样就生成了一段 js 语法的文档,返回给客户端。
  3. 最后,在客户端浏览器中解析 script 标签,并执行返回的 JavaScript ,此时数据作为参数,传入到了客户端预先定义好的回调函数里。

下面是具体实现代码:

2.1 前端

<script type="text/javascript">
    function dosomething(jsondata){
        //处理获得的json数据
    }
</script>
<script src="http://example.com/data?callback=dosomething"></script>

jQuery 写法:

$.ajax({
    url:"",
    dataType:"jsonp",
    data:{
        params:""
        }
}).done(function(data){
    //dosomething..
})

仅仅是客户端使用 jsonp 请求数据是不够的,因为 jsonp 的请求是放在 script 标签中的,它请求到的是一段 js 代码,如果服务端返回了 json 字符串,那么浏览器就会报错。所以 jsonp 返回数据需要服务端做一些处理。

2.2 服务端

下面是 java 实现的 jsonp 的代码:

	String callback = request.getParameter("callback");  
	String output = callback + "({ name : 'John', age : '19'});"

	resp.setContentType("text/javascript");
	PrintWriter out = resp.getWriter();
	out.println(output);
	
	// prints: jsonp1232617941775({"name" : "John", "age" : "19"}); withCredentials

3. CORS

[mozilla HTTP访问控制:CORS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS) 😳😳😳看完这篇 mozilla 的官方 post 后,深感不会比它总结的更准确完善了,so……下文简单介绍,详情直接查看它吧。 另一篇好文:[CORS tutorials](http://www.html5rocks.com/en/tutorials/cors/) 考虑到现代浏览器都支持 CORS 了,所以前端可以直接使用标准的 XMLHttpRequest 发起跨站 HTTP 请求。 CORS 是通过新增一系列 HTTP header,让服务器能声明哪些来源可以通过浏览器访问该服务器上的资源。 Cross-origin requests 分为 simple requests & "not-so-simple requests". ### 3.1 simple requests 所谓简单请求,是指符合如下标准 : * HTTP Method matches (case-sensitive) one of: - HEAD - GET - POST 当是 POST 请求时,Content-Type 必须是如下几种之一: - application/x-www-form-urlencoded - multipart/form-data - text/plain 同时,不能使用自定义请求头(类似于 X-Modified 这种)。 ### 3.2 预请求(not-so-simple requests) “预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。当请求具备以下条件,就会被当成预请求处理: * 请求以 GET, HEAD 或者 POST 以外的方法发起请求。 * 或者使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data, text/plain 以外的数据类型。如: 用 POST 发送数据类型为 application/xml 或者 application/json 的数据的请求。 * 使用自定义请求头(比如添加诸如 X-PINGOTHER) ![preflight](http://www.html5rocks.com/static/images/cors_flow.png) ### 3.3 附带凭证信息的请求 XMLHttpRequest 访问控制功能,最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将 XMLHttpRequest 的一个特殊标志位 withCredentials 设置为true,浏览器就将允许该请求的发送。 ### 3.4 HTTP 响应头 Access-Control-Allow-Origin Access-Control-Expose-Headers Access-Control-Max-Age Access-Control-Allow-Credentials Access-Control-Allow-Methods Access-Control-Allow-Headers ### 3.5 HTTP 请求头 Origin Access-Control-Request-Method Access-Control-Request-Headers ### 3.6 Spring MVC 对 CORS 的支持 Spring offers two methods to support CORS: Controller method CORS configuration & Global CORS configuration, And global and controller level CORS configurations can also be combined. #### solution 1 : Controller method CORS configuration you just have to add a @CrossOrigin annotation to the handler method: `src/main/java/hello/GreetingController.java` ```java @CrossOrigin(origins = "http://localhost:9000") @RequestMapping("/greeting") public @ResponseBody Greeting greeting(@RequestParam(required=false, defaultValue="World") String name) { System.out.println("==== in greeting ===="); return new Greeting(counter.incrementAndGet(), String.format(template, name)); } ``` You can customize this behavior by specifying the value of one of the annotation attributes: origins, methods, allowedHeaders, exposedHeaders, allowCredentials or maxAge. #### solution 2 : Global CORS configuration you can also define some global CORS configuration as well. `src/main/java/hello/GreetingController.java` ```java @RequestMapping("/greeting-javaconfig") public @ResponseBody Greeting greetingWithJavaconfig(@RequestParam(required=false, defaultValue="World") String name) { System.out.println("==== in greeting ===="); return new Greeting(counter.incrementAndGet(), String.format(template, name)); } ``` `src/main/java/hello/Application.java` ```java @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9000"); } }; } ``` ### 3.7 Filter ```java public class CORSFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) {} public void destroy() {} } ``` ### 3.8 nginx 可以通过在 Nginx 上配置,来达到 CORS 的目的,[参考文章](http://enable-cors.org/server_nginx.html) ### 2.3 方案对比 1. JSONP 只支持 GET 请求,而 CORS 支持所有类型的 HTTP 请求,`当使用 RESTful 架构时 JSONP 就更显得力不从心了`。 2. 使用CORS,开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据,比起 JSONP 有更好的错误处理。 3. JSONP 主要被老的浏览器支持,它们往往不支持 CORS ,而绝大多数现代浏览器都已经支持了 CORS 。 所以,抛弃 JSONP,投入 CORS 的怀抱吧!!!