WEB跨域与同源

这几天遇到跨域同源的问题。整理下知识点。

同源策略

浏览器有一个很重要的概念——同源策略(Same-Origin Policy)。所谓同源是指,域名,协议,端口相同。不同源的客户端脚(javascript、ActionScript)本在没明确授权的情况下,不能读写对方的资源。

如果Web世界没有同源策略,当你登录Gmail邮箱并打开另一个站点时,这个站点上的JavaScript就可以跨域读取你的Gmail邮箱数据,这样整个Web世界就无隐私可言了。

不同源举例:

1
2
3
4
5
6
7
8
9
10
11
# 同源要求同域名同协议同端口
域名:
http://www.bigertech.com
http://bigertech.com
http://www.bigertech.cn

协议:
httphttps

端口
127.0.0.1:3000 127.0.0.1:3001

常见的:

我的 A 站点,发了个 AJAX 请求到 B 站点,会收到这样的错误

1
No 'Access-Control-Allow-Origin' header is present on the requested resource

解决方法

1、服务器返回CORS响应头

Rails 方式

1
2
3
4
5
6
7
8
after_action :set_access_control_headers

def set_access_control_header
  headers['Access-Control-Allow-Origin'] = '*'
  headers['Access-Control-Request-Method'] = "GET, POST, PUT, PATCH, OPTIONS, DELETE, HEAD, TRACE"
  headers['Access-Control-Allow-Headers'] = 'Origin, Access-Control-Allow-Headers, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie'
    headers['Access-Control-Allow-Credentials'] = 'true'
end

当然这里偷懒指定了所有域名都允许跨域,一般来说是指定我们允许的域名

JAVA 方式

1
2
3
4
5
6
7
8
9
10
11
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  HttpServletResponse response = (HttpServletResponse) servletResponse;
  String origin = (String) servletRequest.getRemoteHost()+":"+servletRequest.getRemotePort();
  response.setHeader("Access-Control-Allow-Origin", "*");
  response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, OPTIONS, DELETE, HEAD, TRACE");
  response.setHeader("Access-Control-Max-Age", "3600");
  response.setHeader("Access-Control-Allow-Headers", "Origin, Access-Control-Allow-Headers, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie");
  response.setHeader("Access-Control-Allow-Credentials","true");
  filterChain.doFilter(servletRequest, servletResponse);
}

当然,也可以使用 Spring 提供的 corsFilter,或使用注解等等方式

说明

Access-Control-Allow-Origin

它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

在前端 JS 也要设置该值:

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值

Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

关于 Options 请求

浏览器将 CORS 请求分为两大类: 简单请求 和 非简单请求。

只要同时满足以下两个条件,则为简单请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
1) 请求方法是以下三种方法之一:

HEAD
GET
POST

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

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

如果是非简单请求,那么在请求之前,浏览器会发出一条 OPTIONS 预检请求,如:

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

因此,服务端必须要处理 OPTIONS 请求的情况

Rails

1、使用 rack-cors gem

2、自己实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
before_action :cors_set_access_control_headers

def cors_preflight_check
  if request.method == 'OPTIONS'
    cors_set_access_control_headers
    render text: '', content_type: 'text/plain'
  end
end

protected

def cors_set_access_control_headers
  response.headers['Access-Control-Allow-Origin'] = '*'
  response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
  response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
  response.headers['Access-Control-Max-Age'] = '1728000'
end


#routes
match '*all', controller: 'application', action: 'cors_preflight_check', via: [:options]

Java

  1. 默认在 Spring3.x 起,就会自动处理 Options 的请求了,所以可以不用做什么操作。
  2. 在一些旧版本,可以考虑在 filter 监测 options 请求,返回 true

2、使用JSONP方式

JSONP(JSON with Padding)是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)

虽然浏览器有同源策略,但是HTML中的img,script等标签是不用遵循同源策略的, JSONP 则是利用了 script 这个特点。

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
# callback 是 rails 直接支持 JSONP 的参数
# hello 一般是客户端给过来的 callback 函数名
# `callback函数` 将在客户端进行执行 script
def index
   render json: {hello: :world}, callback: hello
end

# 关闭 csrf 防护
# 因为返回格式为 xxx(),属于危险字符script
# protect_from_forgery with: :exception

# 返回结果
/**/hello({"hello":"world"})

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 方式1
<script src="localhost:3001/students">

# 方式2 ( 使用 jquery )
function hello(){
  console.log("something");
}

$.ajax({
    type: 'GET',
    jsonp: 'callback函数名',
    dataType: "jsonp",
    url: "http://localhost:3001/students",
    success: function(data) {
       # 一旦返回将会自动执行hello()
    },
    error: function(XMLHttpRequest, textStatus) {
      console.log("error");
    }
});

Tips

虽然说有同源策略的存在,站点 A 拿不到站点 B 的信息。但其实站点 B 已经处理完请求 response 回去了,只不过被站点 A 的浏览器给拦截了。

1
2
3
4
# 站点 B,此时已经成功处理请求返回 response 了
Started GET "/students" for ::1 at 2015-04-21 20:32:32 +0800
Processing by StudentsController#index as */*
Completed 200 OK in 1ms (Views: 0.1ms | ActiveRecord: 0.0ms)

因此,如果你在后台代码中 / Shell 中进行访问请求的话:

1
2
3
4
# 是成功返回信息的
$ curl http://localhost:3001/students

=>   /**/hello({"hello":"world"})%

有人说这样子那同源策略有什么用?不是一下子就被破解了么?

首先,同源策略是浏览器的行为。

其次,当你登录了网银后,打开站点A,此时站点A网银发送一条request,注意,此时是会附带你网银cookies过去的,相当于你在网银自己发的request一样,如果没有同源策略,这时你的网银就相当于别人的了。。。(当然网银的安全程度远远大于此- -)

而如果你用后台进行发送请求的话,是不会附带任何cookies的。因此也就无效了。

注意 postman是可以成功请求的。(不知道postman做了什么,总之postman没有阻止跨域的情况)

XSS 与 CSRF

说起安全,这两者可是 WEB 届大名鼎鼎的漏洞明星啊!

XSS : 跨站脚本攻击

CSRF : 跨站请求伪造攻击。

以下這篇文章写得非常好,值得一看。 http://snoopyxdy.blog.163.com/blog/static/60117440201281294147873/