web

写在前面

最近在和客户端同学合作开发的时候遇到了几个问题,在客户端(Android Webview)里面内嵌了一个网页,当网页去请求数据接口的时候,Ajax 回调始终进入到了 error 里面,但是 charles 抓包看数据内容和格式又是都是正确的,后来就在 Mac chrome 里面来请求看下情况,表现和客户端一致,在 chrome 控制台的 Network 下的 Response 中没有显示返回的数据,而是显示This request has no response date available.到 Console 里面一看,大红色的No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'xxx' is therefore not allowed access.原来是跨域了。那我们就加上允许跨域头呗,这个加跨域头的步骤比想象中的要复杂点,因为 HTTP Method 不是简单的 GET,而是 POST,还需要加上允许跨域的 HTTP方法。以为到这里就可以了,但是事情还是没有解决,这个时候能看到数据了,但是程序提示没有登录。这是因为在客户端里面操作,所以登陆是由客户端来完成的,然后将凭证信息写入 cookie 中,很明显这个时候 cookie 没有带过去。有搞了一下发现跨域 Ajax 如果需要附带凭证信息的话,需要加上这个参数withCredentials = true,至此问题已经解决。下面详细说一下HTTP 跨域访问,了解了以后,这些问题就都很简单了。

跨域 HTTP请求

跨域 HTTP请求是指被请求的资源的所在域不同于发起该请求的资源的所在域。比如说,域名 A的某个 Web 应用程序通过 img 标签引用了域名 B 的一张图片资源,那么域名A 的应用程序就会导致浏览器发起一个跨域 HTTP请求,在现在的开发当中,我们会使用跨域 HTTP 请求加载各类资源(image、CSS、Javascript和一些数据资源)。

但是处于安全考虑,浏览器会限制JavaScript 脚本发起的跨域请求,表现我们上面已经提到了,通过抓包工具是能够看到数据内容的,但是浏览器里面会显示This request has no response date available。这是因为 XMLHttpRequest对象发起的 HTTP 请求必须遵守同源策略,也就是说 WEB 应用程序只能通过 XMLHttpRequest对象发起同域请求,儿不能发起跨域 HTTP 请求。但是这种需求是存在的,我们经常会需要去加载跨域资源。因此,W3C 的 Web 应用工作组( Web Applications Working Group )推荐了一种新的机制,即跨源资源共享(Cross-Origin Resource Sharing (CORS)),这种机制让Web应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。需要特别注意的是,这个规范是针对API容器的。比如说,要使得 XMLHttpRequest 在现代浏览器中可以发起跨域请求。浏览器必须能支持跨源共享带来的新的组件,包括请求头和策略执行。

概述

跨源资源共享标准通过新增一系列 HTTP 头,让服务器能声明那些来源可以通过浏览器访问该服务器上的资源。另外,对那些会对服务器数据造成破坏性影响的 HTTP 请求方法(特别是 GET 以外的 HTTP 方法,或者搭配某些MIME类型的POST请求),标准要求浏览器必须先以 OPTIONS 请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 Cookies 和 HTTP 认证相关数据)。

一些访问控制场景

在此,我们会用三个场景来解释跨源共享是怎么运行的。其中,所有的跨站请求都是通过 XMLHttpRequest 对象发起,这里为了简单起见,我们使用 jQuery。

简单请求

所谓的简单,是指:

  • 只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。
  • 不会使用自定义请求头(类似于 X-Modified 这种)。

比如说http://127.0.0.1:8088 的 Web 应用想要请求 xxx 的资源

$.ajax({
    url: 'http://xxx.com/res',
    success: successCallback,
    error: errorCallback
});

那么这个请求的请求和响应如下:

Remote Address:220.181.57.217:80
Request URL:http://xxx.com/res
Request Method:GET
Status Code:200 OK

Request Headersview source
Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,ja;q=0.2,ru;q=0.2
Cache-Control:no-cache
Connection:keep-alive
DNT:1
Host:api.mocar.cn
Origin:http://127.0.0.1:8088
Pragma:no-cache
User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53

Response Headersview source
Access-Control-Allow-Origin:*
Access-Control-Expose-Headers:Location
Connection:close
Content-Encoding:gzip
Content-Type:application/json
Date:Fri, 30 Jan 2015 15:43:03 GMT
Server:Apache-Coyote/1.1
Transfer-Encoding:chunked
Vary:Accept-Encoding

请求头中设置了 Origin: http://127.0.0.1:8088,表明在请求来自 http://127.0.0.1:8088, 响应头中设置了Access-Control-Allow-Origin: *,这表明服务器接受来自任何站点的跨站请求。

如果服务器端想要仅允许来自 http://127.0.0.1:8088 的跨站请求,它可以返回:Access-Control-Allow-Origin: http://127.0.0.1:8088。 如上,通过使用 Origin 和 Access-Control-Allow-Origin 就可以完成最简单的跨站请求。不过 Access-Control-Allow-Origin 需要为 * 或者包含由 Origin 指明的站点。

预请求

不同于上面讨论的简单请求,“预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。 当请求具备以下条件,就会被当成预请求处理:

  • 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。
  • 使用自定义请求头(比如添加诸如 X-PINGOTHER)

比如说http://127.0.0.1:8088 的 Web 应用想要请求 xxx 的资源

$.ajax({
    method: 'POST',
    headers: {'Access-Token': accessToken},
    url: 'http://xxx.com/res',
    data: data,
    success: successCallback,
    error: errorCallback
});

那么这个请求的请求和响应如下

  • 预请求

      Remote Address:220.181.57.217:80
      Request URL:http://xxx.com/res
      Request Method:OPTIONS
      Status Code:200 OK
    
      Request Headersview source
      Accept:*/*
      Accept-Encoding:gzip, deflate, sdch
      Accept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,ja;q=0.2,ru;q=0.2
      Access-Control-Request-Headers:access-token
      Access-Control-Request-Method:POST
      Cache-Control:no-cache
      Connection:keep-alive
      DNT:1
      Host:api.mocar.cn
      Origin:http://127.0.0.1:8088
      Pragma:no-cache
      User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53
    
      Response Headersview source
      Access-Control-Allow-Headers:access-token
      Access-Control-Allow-Methods:POST
      Access-Control-Allow-Origin:http://127.0.0.1:8088
      Access-Control-Max-Age:1728000
      Connection:close
      Content-Encoding:gzip
      Content-Length:20
      Content-Type:text/plain; charset=UTF-8
      Date:Fri, 30 Jan 2015 15:43:25 GMT
      Server:Apache-Coyote/1.1
      Vary:Accept-Encoding
    
  • 真实请求

      Remote Address:220.181.57.217:80
      Request URL:http://xxx.com/res
      Request Method:POST
      Status Code:201 Created
        
      Request Headersview source
      Accept:*/*
      Accept-Encoding:gzip, deflate
      Accept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,ja;q=0.2,ru;q=0.2
      Access-Token:cfcf67b8134bba4eb375fa745baec379
      Cache-Control:no-cache
      Connection:keep-alive
      Content-Length:273
      Content-Type:application/json
      DNT:1
      Host:api.mocar.cn
      Origin:http://127.0.0.1:8088
      Pragma:no-cache
      User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53
    
      Request Payloadview source
      Object
    
      Response Headersview source
      Access-Control-Allow-Credentials:true
      Access-Control-Allow-Origin:http://127.0.0.1:8088
      Access-Control-Expose-Headers:Location
      Connection:close
      Content-Encoding:gzip
      Content-Length:20
      Content-Type:text/plain; charset=UTF-8
      Date:Fri, 30 Jan 2015 15:43:25 GMT
      Location:http://api.mocar.cn/vrs/users/me/orders/5219
      Server:Apache-Coyote/1.1
      Vary:Accept-Encoding
    

这里我们使用一个 OPTIONS 发送了一个“预请求”。浏览器根据请求参数,决定需要发送一个“预请求”,来探明服务器端是否接受后续真正的请求。 OPTIONS 是 HTTP/1.1 里的方法,用来获取更多服务器端的信息,是一个不应该对服务器数据造成影响的方法。 随同 OPTIONS 请求,以下两个请求头一起被发送:

Access-Control-Request-Headers:access-token
Access-Control-Request-Method:POST

请求头Access-Control-Request-Method可以提醒服务器跨站请求将使用POST方法,而请求头Access-Control-Request-Headers则告知服务器该跨站请求将携带一个自定义请求头access-token。这样,服务器就可以决定,在当前情况下,是否接受该跨站请求访问。 然后根据服务器的响应。该响应表明,服务器接受了客服端的跨站请求。具体是这几行:

Access-Control-Allow-Headers:access-token
Access-Control-Allow-Methods:POST
Access-Control-Allow-Origin:http://127.0.0.1:8088
Access-Control-Max-Age:1728000

响应头Access-Control-Allow-Methods表明服务器可以接受POST的请求方法。请注意,这个响应头类似于HTTP/1.1 Allow: response header,但仅限于访问控制的场景下。而响应头Access-Control-Allow-Headers则表示服务器接受自定义请求头access-token。就像Access-Control-Allow-Methods一样,Access-Control-Allow-Headers允许以逗号分隔,传递一个可接受的自定义请求头列表。最后,响应头Access-Control-Max-Age告诉浏览器,本次“预请求”的响应结果有效时间是多久。在上面的例子里,1728000秒代表着20天内,浏览器在处理针对该服务器的跨站请求,都可以无需再发送“预请求”,只需根据本次结果进行判断处理。

附带凭证信息的请求

XMLHttpRequest和访问控制功能,最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将XMLHttpRequest的一个特殊标志位设置为true,浏览器就将允许该请求的发送。

比如说http://127.0.0.1:8088 的 Web 应用想要请求 xxxxx 的资源,并设置了一个Cookies值。脚步代码如下:

$.ajax({
    url: 'http://xxx.com/res',
    data: data,
    xhrFields: {withCredentials: true},
    success: successCallback,
    error: errorCallback
});

如你所见,我们在上面将XMLHttpRequest的withCredentials标志设置为true,从而使得Cookies可以随着请求发送。因为这是一个简单的GET请求,所以浏览器不会发送一个“预请求”。但是,如果服务器端的响应中,如果没有返回Access-Control-Allow-Credentials: true的响应头,那么浏览器将不会把响应结果传递给发出请求的脚步程序,以保证信息的安全。

那么这个请求的请求和响应如下:

Remote Address:220.181.57.217:80
Request URL:http://xxx.com/res
Request Method:POST
Status Code:200 OK

Request Headersview source
Accept:*/*
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,ja;q=0.2,ru;q=0.2
Cache-Control:no-cache
Content-Length:28
Content-Type:application/x-www-form-urlencoded
Cookie:uid=19283023
DNT:1
Host:xxx.com
Origin:http://127.0.0.1:8088
Pragma:no-cache
Proxy-Connection:keep-alive
User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53

Response Headersview source
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:http://127.0.0.1:8088
Cache-Control:no-cache
Content-Type:application/json; charset=UTF-8
Date:Fri, 30 Jan 2015 13:43:51 GMT
Expires:0
Proxy-Connection:Keep-alive
Server:nginx
Transfer-Encoding:chunked
X-Whom:qmmwx_0

尽管我们在这里指定了http://xxx.com/res的 cookie,但是如果响应中没有这个头Access-Control-Allow-Credentials: true的话,这个响应将会呗浏览器忽略掉。还有一个主要注意的地方就是必须指定明确的Access-Control-Allow-Origin,不能使用通配符*

其他问题

  1. 在某些版本的Android浏览器中,因为缓存的原因,第一次进行跨域请求是正常的,但是第二次进行的时候则会失效,对于这个问题,可以通过在Header中增加Cache-Control: no-cache 阻止缓存的方式来解决这个问题

参考链接

  • http://stackoverflow.com/questions/1652850/android-webview-cookie-problem
  • https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS