这几天遇到跨域
跟同源
的问题。整理下知识点。
同源策略
浏览器有一个很重要的概念——同源策略(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
协议:
http、https
端口
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
2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/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
- 默认在 Spring3.x 起,就会自动处理 Options 的请求了,所以可以不用做什么操作。
- 在一些旧版本,可以考虑在 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/