一、前言
浏览器为什么会有同源策略(same-origin-policy),这里先记录一个原因:避免跨站请求伪造(Cross-site request forgery )。通常缩写为 CSRF 或者 XSRF。
当然,同源策略仅适用于javascript,这意味着某网站可以通过相应的 HTML标签 访问不同来源网站上的图像、CSS和动态加载脚本等资源。而通常意义上的跨站请求伪造就是利用同源策略不适用于HTML标签的缺陷,这里按下不表。
二、什么是同源策略
同源策略是指在Web浏览器中,允许某个网页脚本访问另一个网页的数据,但前提是这两个网页必须有相同的协议、主机名和端口号,一旦两个网站满足上述条件,这两个网站就被认定为具有相同来源。下图标红的代表origin,如果相同,则代表同源,否则就表示跨源。
下表列出哪些URL与URL http://www.example.com/dir/page.html
属于相同来源:
URL | 结果 | 原因 |
---|---|---|
http://www.example.com/dir/page2.html | 是 | 只有路径不同 |
http://www.example.com/dir2/other.html | 是 | 只有路径不同 |
http://username:password@www.example.com/dir2/other.html | 是 | 只有路径不同 |
http://www.example.com:81/dir/other.html | 否 | 不同端口(若未标明,http:// 默认端口号为80) |
https://www.example.com/dir/other.html | 否 | 不同协议(https和http) |
http://en.example.com/dir/other.html | 否 | 不同域名 |
http://example.com/dir/other.html | 否 不同域名 | (需要完全匹配) |
http://v2.www.example.com/dir/other.html | 否 | 不同域名(需要完全匹配) |
三、如果没有同源策略
如果 AJAX(Asynchronous JavaScript and XML")不遵守同源策略,那么发起攻击过程如下: 我们发起的每一次 HTTP 请求都会全额发送 request 地址对应的 cookie,
- 那么用户登录了自己的银行网站
http://bank.com
,http://bank.com
向客户端的cookie中添加用户标识 - 用户浏览了恶意页面
http://evil.com
。 执行了页面中的恶意 AJAX 请求代码 - 恶意页面向
http://bank.com
发起 AJAX 请求,请求的同时携带了http://bank.com
对应 cookie - 银行页面从 cookie 中判断用户身份无误,返回请求数据。
四、跨源解决方法:
1. CORS(cross-origin-resource-sharing)跨源资源共享
CORS 是一个系统,由一系列 HTTP 头组成,CORS 给了 Web 服务器这样的权限,即服务器可以选择,允许跨源请求访问到它们的资源。由他们决定浏览器是否阻止前端JS代码获取跨源请求的响应。
服务器在响应头中设置以下值即可控制跨源请求:
HTTP头 | 作用 | 备注 |
---|---|---|
Access-Control-Allow-Origin | 指示请求的资源能共享给哪些域。 | 服务器设置必填。 |
Access-Control-Allow-Credentials | 该响应头只能是true或者不设置! | 服务器设置必填。注意,当设置了 Access-Control-Allow-Credentials 为 true 时,Access-Control-Allow-Origin 不能为*。比如:在cookie中存取的是用户的登录信息,又不限制客户端的请求来源,他人获取到cookie以后则可随意发起请求,登录该用户账号,损害用户权益 |
Access-Control-Allow-Methods | 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。 | 服务器设置必填。 |
Access-Control-Allow-Headers | 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。 | |
Access-Control-Expose-Headers | 指示哪些 HTTP 头的名称能在响应中列出。 | |
Access-Control-Max-Age | 指示预请求的结果能被缓存多久。 | |
Access-Control-Request-Headers | 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。 | 浏览器发送预检请求时携带 |
Access-Control-Request-Method | 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。 | 浏览器发送预检请求时携带 |
Origin | 指示获取资源的请求是从什么域发起的。 |
CORS 规定浏览器的请求分为简单请求和预检请求
-
简单请求
同时满足以下条件:
- 请求方法是
HEAD
,GET
,POST
三种方法之一 - HTTP 的头信息不超出以下几种字段:
Accept
,Accept-Language
,Last-Event-ID
,Content-Language
Content-Type
:只限于三个指定值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器 - 请求中没有使用
ReadableStream
对象
- 请求方法是
-
预检请求
pre-flight request
- 凡是不同时满足上面两个条件,就属于非简单请求。
- 在发送正式 HTTP 请求前,浏览器会先发送 OPTIONS 请求,得到返回结果后在决定是否发送正式请求。
-
一般来说,浏览器不借助 JS 脚本能发出的请求都属于简单请求,包括 form 表单的 post 请求
2. JSONP 跨源
利用 script 标签的 src 属性不受同源策略限制的特性,可以向服务器发起 get 请求,需要和服务端协商把数据以回调函数参数的形式发送给前端。目前这种方法已经过时。
下面是一个简化版例子:
//服务端
const responseData = { data: [{ id: 1, name: 'Brian' }, { id: 2, name: 'Peter' }] }
const responseDataText = JSON.stringify(responseData)
app.get('/josn',(req,res)=>{
const callback=new Url(req.url,`http://${request.headers.host}`).searchParams('cb')
res.set('Content-Type','Application/json')
res.send(`${callback}(${responseDataText})`)
})
//浏览器端
function sendJsonp(url){
return new Promise(resolve => {
let script= document.createElement('script')
script.src=url
document.body.append(script)
//jsonpCallback函数必须挂载在全局对象上
window['_jsonpCallback']=function(data){
resolve(data)
}
})
}
sendJsonp('//localhost:3000/json?cb=foo').then((data)=>{
console.log(data) //{ data: [{ id: 1, name: 'Brian' }, { id: 2, name: 'Peter' }] }
})
3. 代理跨源
将跨源请求转发给同域服务器,由服务器转发请求给目标服务器,数据返回后再返回给浏览器。由于服务器之间没同源策略,所以通过服务器代理的方式获取数据。