跨域问题
问题背景
因为浏览器出于安全考虑,有同源策略。如果协议、域名 或者端口
有一个不同就是跨域,Ajax 请求会失败。
主要是用来防止CSRF攻击
的,避免攻击者利用用户的登录态
通过脚本等方式去获取非同源的恶意数据。
一个典型的CSRF攻击有着如下的流程:
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
在没有同源策略的情况下,网站可以被任意来源的Ajax访问到内容,如果此时还存在登录态,那么对方可以通过Ajax
获取你的任何信息。
那么,请求到底发出去了没有?发出去了,只是浏览器觉得不安全,所以拦截了响应。
跨域解决方案
JSONP
利用<script>
标签没有跨域限制的漏洞。
通过<script>
指向一个需要访问的地址并提供一个回调函数来接收数据。
- 使用简单且兼容性不错
- 只限于
get
请求
function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script');
script.src = url;
script.async = true;
script.type = 'text/javascript';
window[jsonpCallback] = function(data) {
success && success(data);
}
document.body.appendChild(script);
}
jsonp('http://xxx', 'callback', function(value) {
console.log(value);
})
// 后端返回 包裹数据的函数调用
callback({...具体的数据})
CORS
- 需要浏览器和后端同时支持,关键在后端
IE8/9
需要通过XDomainRequest
来实现
服务端设置响应头Access-Control-Allow-Origin
就可以开启CORS
,该属性表示哪些域名可以访问资源,如果设置通配符则表示所以的网站都可以访问资源。
这套机制建立在一个核心内容基础上:
http headers
,定义了一系列的HTTP头,通过这些HTTP头来控制资源的访问。 跨源资源共享(CORS) - HTTP | MDN
通过这种方式解决跨域问题,发送请求时会有两种情况:
- 简单请求(包括以下条件)
- 使用一下方法:
GET/HEAD/POST
Content-Type
为以下三者之一:text/plain
、multipart/form-data
、application/x-www-form-urlencoded
- 使用一下方法:
- 复杂请求
- 先发送method为options的请求,称为
预检请求
,后端需要对这个预检请求进行处理,返回对应的头信息,告知客户端是否允许发送非简单请求。
- 先发送method为options的请求,称为
对于预检请求来说,如果你使用过 Node 来设置 CORS 的话,可能会遇到过这么一个坑。
(见下方代码)
该请求会验证你的 Authorization 字段,没有的话就会报错。
此时预检请求也会进入回调中,也会触发 next 方法,因为预检请求并不包含 Authorization 字段,服务端就会报错。
所以,需要在回调中过滤 option 方法。
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods','PUT, GET, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials');
next();
})
document.domain
适用于二级域名相同的情况下,比如a.test.com
和b.test.com
只需要给页面添加document.domain = 'test.com'
即可表示二级域名都相同就可以实现跨域
postMessage
这种方式通常用于获取嵌入⻚面中的第三方⻚面数据。一个⻚面发送消息,另一个⻚面判断来源并接收消息。包含有以下几种场景:
- 页面和其打开的新窗口的数据传递
- 多窗口之间的消息传递
- 页面与嵌套iframe消息传递
- 上面三种场景的跨域数据传递
// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', event => {
var origin = event.origin || event.originalEvent.origin;
// 如果是目标地址,通过,执行相关操作
if (origin === 'http://test.com') {
console.log('验证通过');
}
});
跨域解决方案:后端代理
除了js能通过一些API发送HTTP请求,其他语言也可以实现,同源策略仅仅是浏览器下的某种策略机制,其他语言不受限制,所以我们可以利用静态资源(也就是js发送请求所在的资源服务器)去发送请求,然后进行转发,我们称为“代理”。
- 正向代理
- {客户端脚本 (xhr)} ⇒ {API服务器} :跨域
- {客户端脚本(xhr)⇒ 代理 } ⇒ {API服务器}:跨域
- 反向代理
- {客户端(xhr)} ⇒ { 代理 ⇒ API服务器 } :跨域
- 使用第三方中间件
- koa-server-http-proxy(正向代理)
const Koa = require('koa')
const app = new Koa()
const proxy = require('koa-server-http-proxy')
app.use(proxy('/api', {
target: 'https://news-at.zhihu.com',
//把当前请求的path转成目标服务器实际path
pathRewrite: { '^/api': '' },
changeOrigin: true
}))
app.listen(3000)
跨域凭证信息处理
- 基于Cookie的CORS处理
- cookie实际上也会受到同源策略的限制,如果是非同源请求,cookie是默认被禁止携带的。
- 处理方式
- 客户端在请求中设置:
withCredentials : true
- 服务端要在cros中设置:
ctx.set('Assess-Control-Allow-Credentials' , 'true')
- 客户端在请求中设置:
- 基于Token的鉴权机制
- 基于Cookie的验证会存在一些问题:
- 无论当前请求是否需要传输Cookie,浏览器都会主动发送(浪费)
- 容易被CSRF攻击
- 使用JWT
- Json web token,为了在网络应用环境间传递声明而执行的一种基于JSON的开发标准。JWT.IO - JSON Web Tokens Introduction
- 组成结构:由通过
.
链接的三段文本信息构成
- 基于Cookie的验证会存在一些问题:
let header = {
"alg": "HS256",
"typ": "JWT"
}
let payload = {
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
let sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
let token = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}.${sign}`;
- 使用jsonwebtoken
//生成token
const jwt = require('jsonwebtoken');
let key = 'merlin'
let token = jwt.sign({ uid: 1 }, key);
ctx.set('Aythorization',token)
//验证token
try {
let uid = jwt.verify(token, key);
} catch(err) {
// err
}