同源策略
含义
A 网页设置的Cookie,B网页不能打开,除非这两个网页同源。所谓同源是指
- 协议相同
- 域名相同
- 端口相同
举个来说,http://www.abc.com/index
这个网址,协议是http://
,域名是www.abc.com
,端口是80
(默认端口可以省略)
http://www.abc.com/index2
: 同源https://www.abc.com/index
: 不同源(协议不同)http://www.abcde.com/index
:不同源(域名不同)http://www.abc.com:81/index
:不同源(端口不同)
目的
同源策略的目的,是为了保证用户的安全,防止恶意的网站窃取数据。
限制范围
目前如果非同源,共有三种行为收到限制。
Cookie
、LocalStorage
、IndexDB
无法读取DOM
无法获得AJAX
请求不能发送
跨域请求的安全问题
通常,浏览器会对上面提到的跨域请求作出限制。浏览器之所以要对跨域请求作出限制,是出于安全方面的考虑,因为跨域请求有可能被不法分子利用来发动 CSRF攻击。
CSRF攻击:
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被成为:one click attack/session riding,缩写为:CSRF/XSRF。CSRF攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任,以用户身份在攻击页面对目标网站发起伪造用户操作的请求,达到攻击目的。
CSRF 攻击的原理大致描述如下:有两个网站,其中A网站是真实受信任的网站,而B网站是危险网站。在用户登录了收信人的A网站时,本地会存储A网站相关的Cookie,并且浏览器也维护这一个Session会话。这时,如果用户在没有登出A网站的情况下访问危险网站B,那么危险网站B就可以模拟发出一个对A网站的请求(跨域请求)对A网站进行操作,而在A网站的角度来看是并不知道请求是由B网站发出来的(Session和Cookie均为A网站的),这时便成功发动一次CSRF 攻击。
因此,大多数浏览器都会跨域请求作出限制,这是从浏览器层面上的对 CSRF 攻击的一种防御,但是需要注意的是在复杂的网络环境中借助浏览器来防御 CSRF 攻击并不足够,还需要从服务端或者客户端方面入手防御。详细可以参考这篇文章浅谈CSRF攻击方式
跨域正确打开方式
服务端
const Koa = require('koa');
const route = require('koa-route');
const app = new Koa();
const fs = require('fs');
const main = async ctx=>{
ctx.response.body = await fs.createReadStream('./1.json')
}
const add = async ctx=>{
await ctx.req.addListener('data', (data) => { // 有数据传入的时候
console.log(data,'aa')
});
}
app.use(route.get('/',main))
app.use(route.post('/add'),add)
app.listen(3300)
上面代码可以看到我们的服务端口是
3300
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="div">
</div>
<button class="button">---</button>
<button class="add">++</button>
</body>
</html>
<script src="./node_modules/axios/dist/axios.min.js"> </script>
<script>
let div = document.getElementById('div')
let button = document.getElementsByClassName('button')[0];
let add = document.getElementsByClassName('add')[0];
let fn = ()=>{
axios.get('http://192.168.10.40:3300/').then(res=>{
let {data} = res;
let str = ``;
data.forEach(item=>{
str+=`<p style="color:'red';fonst-size:'30px'">${item.title}</p><p style="color:'aqua'">${item.content}</p>`
})
div.innerHTML = str;
})
}
fn()
button.onclick = function(){
let index = 1
axios.get(`http://192.168.10.40:3300/delete?index=${index}`).then(res=>{
console.log(res,'aaa')
let {data} = res;
data == 1&& fn()
})
}
</script>
当我们执行这段代码时,控制台会报错
Access to XMLHttpRequest at 'http://---:3300/' from origin 'http://---:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
,因为两个地址的端口不一样,所以会造成跨域
这个错误就是提示跨域
解决跨域的方式
jsonp
jsonp是一种跨域通信的手段,特德原理其实很简单:
- 首先是利用
script
标签的src
属性来实现跨域。 - 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后在返回,实现服务端向客户端通信。
- 由于使用
script
标签的src
属性,因此只支持get
方法
服务端
const Koa = require('koa');
const route = require('koa-route');
const app = new Koa();
const fs = require('fs');
const main = async ctx=>{
let _data = await fs.readFileSync('./1.json','utf-8')
let {query} = ctx.request;
ctx.body = `${query.callback}(${JSON.stringify(_data)})`
}
const add = async ctx=>{
await ctx.req.addListener('data', (data) => { // 有数据传入的时候
console.log(data,'aa')
});
}
app.use(route.get('/',main))
app.use(route.post('/add'),add)
app.listen(3300)
客户端
function jsonp({url,params,callback}) {
return new Promise((resolve,reject)=>{
let _script = document.createElement('script');
window[callback] = function(data) {
resolve(JSON.parse(data))
}
params = {...params,callback};
let _arr = [];
for(let key in params){
// 属性值与属性名以等号方式拼接在一起
_arr.push(`${key}=${params[key]}`)
}
_script.src = `${url}?${_arr.join('&')}`;
document.body.appendChild(_script);
})
}
jsonp({
url:'http://192.168.10.40:3300/',
params:{},
callback:'show'
}).then(res=>{
console.log(res,'数据')
})
cors
cors 与 jsonp的区别
jsonp
只能实现get
请求,而cors
支持所有类习惯的HTTP
请求.- 使用
cors
,开发者可以使用普通的XMLHttpRequest
发起请求和获取数据,比起jsonp
有更好的错误处理。
cors对于浏览器发过来的AJAX跨域七个球分为两种
- 简单请求
- 非简单请求
简单请求
所谓简单请求就是
HEAD
,get
,post
三种请求方式
浏览器会在header信息里卖弄多加一个字段:
key:origin
value: 协议+域名+端口
服务器根据对象里的origin
值来决定是否同意这次请求
如果请求通过
请求通过后,服务器会在header里多增加几个字段:
Access-Control-Allow-Origin: https://localhost:8080
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Token
上面的头信息之中,有三个与cors请求相关的字段,都以Access-Control-
开头。
- Access-Control-Allow-Origin:
该字段是必须的。他的值要么是请求时origin
字段的值,要么时一个*
,标识接收任意域名的请求 - Access-Control-Allow-Credentials:
该字段可选。他的值是一个布尔值,标识是否允许发送Cookie
。默认情况下,Cookie
不包括在CORS
请求之中。设为true
,即表示服务器明确许可,Cookie
可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务武器不要浏览器发送Cookie
,删除该字段即可 - Access-Control-Expose-Headers:该字段可选。
cors
请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面制定。
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在 证是通信之前,增加一次HTTP查询请求,成为预检请求
axios.put('http://192.168.10.40:3300/').then(res=>{
})
上面代码中,
HTTP
请求的方法是PUT
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的信息
"预检"请求用的请求方法是OPTTIONS
,标识这个请求时用来查询的。头信息里面,关键字段时origin
,标识请求来自哪个源。除了origin
字段,"预检"请求的头信息包括两个特殊的字段
- Access-Control-Request-Method:该字段时必须的,用来列出浏览器的CORS请求会用到那些
HTTP
方法,上列是PUT
- Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器
CORS
请求会额外发送的头信息字段。
预检请求的回应
浏览器收到"预检"请求以后,检查了
origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨域请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的HTTP
回应中,关键的是Access-Control-Allow-Origin
字段,表示http://api.bob.com
可以请求数据。该字段也可以设为*
,表示统一任意跨源请求。
Access-Control-Allow-Origin: *
如果浏览器否定了"预检"请求,会返回一个正常的HTTP
回应,但是没有任何CORS
相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误。
Access-Control-Allow-Methods
该地段必须,它的值是逗号分隔的一个字符串,表名服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers
如果浏览器请求包括
Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
地段是必须的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
Access-Control-Allow-Credentials
与简单请求时的含义相同
Access-Control-Max-Age
该字段可选,用来制定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应20天(1728000秒),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应
一旦浏览器通过了"预检"请求,以后每次浏览器正常的
CORS
请求,就都跟简单请求一样,会有一个origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
服务端
const Koa = require('koa');
const route = require('koa-route');
const app = new Koa();
const fs = require('fs');
const main = async ctx=>{
ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); 这里是关键
ctx.response.body = await fs.createReadStream('./1.json');
}
app.use(route.get('/',main))
app.listen(3300)