跨域解决方案

113 阅读15分钟
浏览器为何要设置同源策略?


正是因为浏览器要出于安全考虑。如果缺少了同源策略,浏览器很容易受到XSS和CSFR等攻击。(XSS与CSFR可以单独成为一个额外的知识点) 此时会导致一个域名下网页的操作就可以直接拿到另一个非同域名下网页的任何信息,或者一个网页可以随意请求到不同域名服务器下的接口数据。


什么是同源策略?


同源策略是一种约定,这是浏览器核心的安全功能点之一。所谓的同源策略指的是【协议 + 域名 + 端口】三者相同,如果两个相同的域名指向同一个ip地址,也是非同源的情况。同时地址印射对应的ip两者也是非同源情况。


通过什么方式可以解决跨域?


可以通过JSONP的原理


首先明白对于浏览器加载资源时可以通过:


img


script


link


以上几个标签是允许跨域加载资源的。意思就是在www.baidu.com域名下静态html文件中的script标签可以加载wwww.google.com服务器下的脚本资源等。



通过以上标签可以加载跨域资源的理解,那我们可以通过包装手段从其它域获取到期望的数据。



讲讲JSONP的实现原理?



之前已经有了原理的思路的铺垫。那就利用script标签这一允许跨域加资源的特性包装数据进行讲解。




实现流程




// index.html



// jsonp的实现模拟



[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}




// 调用方式



[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
jsonp({
url: 'http://localhost:3000/getUser',
params: { name: 'peter' },
callback: 'user'
}).then(data => {
console.log(data)
})



复制代码通过以上代码实现了一个基本的JSONP的调用执行代码。





声明一个JSONP的模拟函数, 传入的参数分别为请求地址、请求参数、前后端约定的包装函数名、 内部通过返回promise机制来优雅的解决数据返回的获取方式。





通过script不存在跨域请求资源的机制创建一个script临时标签。把向后台请求的地址和参数组合成query参数的形式。



请求地址: http://localhost:3000/getUser?name=peter&callback=user





关健点是把包装的函数名(key作为callback, value作为user) 包装函数名是前后端一个约定。




最后组装后的script标签插入到document文档中,此时浏览器就会自动向标地址发起请求。





后台返回的结果原理




// app.js 用express脚手架模拟的配合前台callback封装的返回结果



[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.get('/getUser', function(req, res, next) {
let { name, callback } = req.query
console.log(name) // peter
console.log(callback) // user
res.send(`${callback}({
code: 0,
msg: '请求成功',
data: {
id: 1234
}
})`)
});



复制代码后台会通过query参数进行解析。如果此时返回的结果是一个对象,对象中存在msg消息,请求状态码code,数据信息data。



可能你会疑问为什么返回的结果的值是放在一个user执行函数中。这就是JSONP的核心原理。回头再看看这段没有解释的代码段:



[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}



复制代码当执行自己封装的jsonp的方法的时候在全局定义一个函数。此函数名则是前端与后端约定的函数封装名。当后台返回结果时会执行约定好的全局函数。就是执行上方代码段, 数据参数会通过resolve执行返回。最后删除对应的请求script标签。



JSONP和AJAX对比,区别点在那里?




相同点:




JSONP与ajax两者相同点都是客户端向服务端发起请求。




不同点:




JSONP属于利用script标签进行了非同源策略请求,而ajax是同源策略请求。



JSONP优缺点




优点:




JSONP的优点是兼容性很好。因为利用的是script标签可以非同源请求机制。这是每个浏览器基础特性。




缺点:




只支持query参数的这种get请求方式,交互方式存在局限性。也容易受到xss的攻击。



如果后台不支持JSONP的封装方式怎么办?



可以通过CORS网络通信技术。(全称Cross-Orgin Resource Sharing),对于CORS同样也需要前后端进行一个配合。但是关健点在于后台的配置。可能你会认为。即然是后台进行配置,为什么前台也需要充分的了解。因为无论在生产还是开发的模式下, 跨域首先对前端的影响面是最大的, 只有充分的了解才能向后台去表达后台才能准确的设置和进行配合。



简单的跨域请求需要建议后台进行什么设置?




前台模拟设置




先本地创建一个index.html写入请求脚本。通过http-server -p 4000启动在本地4000端口下。



// index.html



[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
let url = 'http://localhost:3000/getUser';
let xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.send();



复制代码



后台模拟设置




通过express框架设置请求地址,服务启动在本地3000端口下。



// app.js



[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let express = require('express')
let app = express()
app.get('/getUser', function(req, res) {
res.send({
code: 0,
msg: '请求成功',
data: {
id: 1234
}
})
})
app.listen(3000)



复制代码



浏览器返回结果




访问http://127.0.0.1:4000/index.html可以通过Network控制台可以看到浏览器端向后台http://localhost:3000/getUser服务接口地址发出请求。



如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。虽然返回的 Status Code 状态码是 200 OK,但是response响应头里并没有返回期望的值。同样在console控制台可以发现:



Access to XMLHttpRequest at 'http://localhost:3000/getUser' from origin 'http://127.0.0.1:4000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.



复制代码CORS策略阻止了从http://127.0.0.1:4000访问http://localhost:3000/getuser处的XMLHttpRequest:请求的资源上没有'Access- control - allow-origin'头。



这就是一个最简单的CORS的安全策略,从报错可以很明显的明白你需要告诉后台需要设置'Access- control-allow-origin'头。




后台解决方案




// app.js中添加




[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
8
9
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
// res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
next()
})




复制代码在接收到请求时做一层中间件的过滤, 以下两者方式皆可。




返回时设置响应头的Access-Control-Allow-Origin为*(代表所有域名向当前服务请求都允许跨域访问)



返回时设置响应头的Access-Control-Allow-Origin为指定的域名。其它域名都不允许进行一个跨域访问




设置Access-Control-Allow-Origin头就可以解决了所有的跨域问题了麻?



Access-Control-Allow-Origin头的设置仅仅只能解决简单的跨域请求




简单的跨域请求条件:




条件1: 只能允许以下的请求方法




GET



HEAD



POST




条件2: Content-Type允许条件




text/plain



multipart/form-data



application/x-www-form-urlencoded




条件3: 不能超过http的头信息以下字段




Accept



Accept-Language



Content-Language



Last-Event-ID




那其它请求方式如何解决?属于什么类型的跨域请求?



其它的请求方式被称之为复杂的跨域请求。一旦不符合简单跨域请求策略的时候那就是复杂的跨域请求:




复杂的跨域请求解释:





除了简单的跨域请求的方法。比如PUT、DELETE



除了简单的跨域请求的Content-type类型。比如application/json



自定义的header头



不同域名下的cookie传输





尝试解决复杂跨域的几种情况




1.put、delete等请求方法造成复杂请求



// 修改请求方法



- xhr.open('get', url, true);



+ xhr.open('put', url, true);



复制代码// 修改后台接收请求方法



app.put('/getUser') // 省略... 对于后台只是把get请求换成put接收请求



复制代码在浏览器的netWork中发现并没有发送put请求,在General中的Request Method发现发送了一个OPIONS的预检请求(关于预检后续会在解决跨域问题中通过关闭浏览器策略中专门介绍相关详细知识点)



同时浏览器中会被发出报错信息:



Access to XMLHttpRequest at 'http://localhost:3000/getUser' from origin 'http://127.0.0.1:4000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.



复制代码解决方案:



// 在app.use中添加新的设置头



// res.setHeader('Access-Control-Allow-Methods', '*')



res.setHeader('Access-Control-Allow-Methods', 'PUT')



复制代码以上设置了接收允许那些请求方法:




设置*, 表示所有请求方法都允许。



设置对应的请求方法以逗号分隔。




2.content-type造成复杂请求



+ xhr.setRequestHeader('content-type', 'application/json');



复制代码在之前谈论简单跨域请求条件二, 关于content-type类型对于简单的跨域请求只支持三种。设置其它的则会产生复杂的跨域请求。当设置content-type: application/json的情况下,同样的浏览器会发出报错信息:



Access to XMLHttpRequest at 'http://localhost:3000/getUser' from origin 'http://127.0.0.1:4000' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.



复制代码从报错提示可以看出后台需要对复杂跨域请求content-type进行一个额外的设置:



// 在app.use中添加新的设置头



+ res.setHeader('Access-Control-Allow-Headers', 'content-type')



复制代码3.自定义头造成复杂请求



+ xhr.setRequestHeader('X-Customer-Header', 'value');



复制代码在之前谈论简单跨域请求条件三中, 除了以上几种http请求头之后,都属于自定义头。在请求带入时会造成复杂的跨域请求, 同样的浏览器会发出报错信息。



Access to XMLHttpRequest at 'http://localhost:3000/getUser' from origin 'http://127.0.0.1:4000' has been blocked by CORS policy: Request header field x-customer-header is not allowed by Access-Control-Allow-Headers in preflight response.



复制代码同样的原理对于前台设置的自定义头后,后台在接收的时候同样也要进行允许设置接收前台自定义传输出来的自定义头。



res.setHeader('Access-Control-Allow-Headers', 'content-type, X-Customer-Header')



// res.setHeader('Access-Control-Allow-Headers', '*')



复制代码在Access-Control-Allow-Headers设置的时候,可以用逗号分隔,进行多个自定义头的设定。同时也可以传入*,允许所任何自定义头。



谈谈CROS中的cookie?




绝对同域的情况下




在绝对同域的情况下。前台向后台请求的接口或者请求文件的时候,会自动把cookie带入请求头中。




在非同域的情况下




在非同域的情况下。需要使用CORS的策略进行传输。默认情况下,cookie并不会带入请求头中,需要对xhr设置请求凭证。



xhr.withCredentials = true



复制代码



简单的跨域请求与cookie




如果此时是简单的跨域请求, 设置withCredentials = true的情况下。请求头中会带入cookie信息, 后台接收请求并且会发送到前台, 此时浏览器端从response中可以看到数据已经返回,但是并不能获取的后台返回的数据, 因为此时会被xhr的错误进行捕获,浏览器控制台会出现以下提示:



Access to XMLHttpRequest at 'http://localhost:3000/getUser' from origin 'http://localhost:4000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.



复制代码



复杂的跨域请求




如果此时是复杂的跨域请求,设置withCredentials = true的情况下。此时会发送一个OPTIONS请求。浏览器发出的错误信息仍然是与简单的跨域请求报错一致。




解决方案




此时前台发送cookie凭证, 同样的后台一样需要同意接收凭证。



res.setHeader('Access-Control-Allow-Credentials', true)



复制代码



反向原理:




如果后台同意接收凭证。而前台没有设置发送凭证的情况下。就算后台发送到前台的响应头中设置了cookie信息(set-cookie头),无论是简单的跨域请求还是复杂的跨域请求都会导致cookie塞入无效,可以查看appliation/cookie中, 不会有后台写入的cookie信息。




保持同源策略




为了安全问题。cookie本质上还是保持了同源策略的模式。在前后台都设置了发送/接收凭证之后, 对于反回的origin头的设置res.setHeader('Access-Control-Allow-Origin', '*') 不能为*, 需要设置成指定请求的来源 res.setHeader('Access-Control-Allow-Origin', req.headers.origin)。




合法组合与非法组合。




当设置Credentials的时候,后台需要知道Access-Control-Allow的合法与非法组合性。



一旦Access-Control-Allow-Credentials设置为true的时候, 此时以下几个不能设置为*, 需要进行指定, 否则以下三者一率视为无效设置。




Access-Control-Allow-Headers



Access-Control-Allow-Origin



Access-Control-Allow-Methods




CORS情况下如何在xhr中拿到响应头中的信息?



可以通过xhr.getResponseHeader方法进行获取。但是此方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。



在后台响应的时候可以响应头中塞入一些自定义的头和值。



res.setHeader('name', 'peter')



复制代码在响应体的报文中可以看到:



Access-Control-Allow-Credentials: true



Access-Control-Allow-Headers: content-type, X-Customer-Header



Access-Control-Allow-Methods: PUT



Access-Control-Allow-Origin: http://localhost:4000



Connection: keep-alive



Content-Length: 50



Content-Type: application/json; charset=utf-8



Date: Sun, 17 Feb 2019 08:18:08 GMT



ETag: W/"32-oUKytSTXnBL0hnySFj9PpHgmBQk"



name: peter // 重点在这里



X-Powered-By: Express



复制代码通过报文可以发现返回的很多之前后台设置的信息和这里最关健的name头信息。但是通过以下方法测试之后结论:



xhr.onreadystatechange = function() {



if (xhr.readyState === 4) {



if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {



console.log(xhr.getResponseHeader('Content-Type'))



console.log(xhr.getResponseHeader('name'))



}



}



}



复制代码在xhr返回成功之后。分别获取两个头信息。





Content-Type 则会返回 application/json; charset=utf-8



name 则会提示报错信息,并且返回null空值。



Refused to get unsafe header "name" // 拒绝获取不安全的头信息“name”



复制代码可以明确的知识,除了之前提到的以上六种头信息可以进行获取之外,其余的一律都需要在后台进行允许那响应些头访问的设置。



res.setHeader('Access-Control-Expose-Headers', 'name')



复制代码此时浏览器中报错信息不会存在,同时也能打印出name在响应头中的值。注意 如果设置的值为 * 则无效。需要对指定字段头进行设置。



复杂的跨域请求会造成每次请求都发送一个OPTIONS请求,如何解决?



通过以上的所有对复杂的跨域请求的分析清楚的认识到,那些请求方式会造成发送预检,一句话概括,**Access-Control-Max-Age 这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久。**这样对network中的请求观察和请求性能来说都不友好。如果做到友好又安全的机制。




对预检进行一个时间请求有效期




res.setHeader('Access-Control-Max-Age', 600)



复制代码对预检请求设置10分钟的过期时间(时间可以根据项目情况进行自定义)。但是对于每个浏览器的缓存时间机制都不一样。在本地调试的时候,有时候你会发现设置了预检的过期时间并不生效。注意一下可能开启了浏览器的Disable cache导致了此原因



在前后端联调时,不通过后端设置,如何解决跨域问题?




关闭浏览器跨域策略。




通过之前分析整个跨域模式是由前台浏览器的所作所为造成的。为了安全,浏览器对跨域请求做了一系列的验证。那是否可以想想, 通过手动关闭浏览器跨域策略是不是可以解决根本性的问题。




Mac 创建一个chrome.sh文件




#!/bin/bash



#!/bin/sh




open -a "Google Chrome" --args --disable-web-security --user-data-dir




exit 0



复制代码通过终端运行:



sh 加上chrome.sh文件地址



复制代码注意:



在运行终端命令的时候,先检查是否已经启动过chrome,如果启动过需要手动关闭整个chrome的进程。