关于跨域的一些解决方案
########跨域(非同源策略请求)
-同源策略请求 ajax/fetch
-跨域传输
2013年以前 前后端不分离
部署到同一个web服务器上:同源策略
-xampp 修改本地的host文件
服务器拆分
web服务器:静态资源 kbs.spotrs.qq.com
data服务器:业务逻辑和数据分析 api.sports.qq.com
图片服务器:
三者都一样就是同源,只要有一个不同就是跨域
-协议
-域名
-端口号
WEB服务器地址:http://127.0.0.1:3000/index.html
数据接口地址:http://127.0.0.1:4000/list
=============================
1.JSONP
-script
-img
-link
-iframe
-...
以上不存在跨域请求的限制
向服务器发送请求,同时把本地的一个函数传递给服务器
服务器接收客户端的请求,准备数据,给客户端返回数据
因此,JSONP需要服务器端的支持
问题:jsonp只能处理get请求
=============================================
2、CORS跨域资源共享
-客户端(发送ajax/fetch请求)
-服务器端设置相关的头信息(需要处理options试探性请求)
====================================================
3、http proxy代理 =>webpack webpack-dev-server
=================================================
4、ngix反向代理 ==>不需要前端干啥
proxy 相当于node给你模拟了一个ngix请求,服务器请求服务器不存在跨域。proxy请求来数据,解决跨域
5、postMessage
=========================
6、WebSocket协议跨域
其余方案:基于iframe的方案
7、document.domain + iframe
只能实现:同一个主域,不同子域之间的操作
8、window.name+iframe
9、location.hash+iframe
需要3个页面,A和C同源,A和B非同源
1、JSONP
- 利用了js脚本不受同源限制。
- 前后端规范统一的脚本方法名,来传递数据执行特定的方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP-demo</title>
</head>
<body>
<p>hello world</p>
<script>
function myFunction (data) {
alert('获取数据成功,2s后改变数据!')
let p = document.getElementsByTagName('p')[0]
setTimeout(function () {
p.innerHTML = data.message
}, 2000)
// 2s后p标签内的内容将改变
}
</script>
<!-- <script src="http://localhost:3001?callback=myFunction"></script> -->
<script >
myFunction({'message': 'hello world from JSONP!?'}) //上面注释掉的等同于下面这段脚本。
</script>
<!--在src里面的问号?后面的参数({callback: 'myFunction'})可以在3001端口页面中可以通过req.query.callback获取-->
</body>
</html>
- 服务端获取方法名
app.get('/', function (req, res) {
var callbackName = req.query.callback; // 获取方法名:myFunction
logger.debug(callbackName)
res.send(callbackName+"({'message': 'hello world from JSONP!?'});");
// myFunction({'message': 'hello world from JSONP!'})
// 返回一个带参数的执行函数
})
总结jsonp
相当于就是一种前后端搭配做到动态执行js脚本,由后端传递参数的办法。
Ajax中的jsonp
- 设置dataType:jsonp
- 设置jsonp:callback
- 设置前端执行的方法名 jsonpcallback:functionName
- 在Ajax中会自动帮你生成回调方法名,所以可以不用指定jsonpCallback,可以直接调用success回调函数
@RequestMapping(value="/user/info")
public String userInfo( @RequestParam String callback){
//参数callback会拿到jquery自动生成的方法名
return callback+"('natural')";//与jsonp一样返回带参方法即可
}
$.ajax({
url:'http://localhost:8080/user/info',
type:'GET',
dataType: 'jsonp',
jsonp:'callback',//默认约定callback
// jsonpCallback:'handleResponse',可以定义自己的方法,否则直接调用success回调方法。
success:function(data){
alert(data)
},error:function(error){
console.log(error)
}
})
function handleResponse (data){ //自己定义的回调方法
let p = document.getElementsByTagName('p')[0];
setTimeout(function(){
p.innerHTML=data
},2000)
}
CORS跨域资源共享
- 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
- 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
- 优点:xmlHttpRequest 的onerror 可以捕获错误信息。(不要以状态码200判断,因为http可能返回200的状态码。也就是请求成功了,但控制台报错)。不单单只是GET方法
- 与jsonp比较:JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 设置response
app.get('/', (req, res) => {
res.set('Access-Control-Allow-Origin', 'http://localhost:3000'); // 设置允许跨域的origin,允许3000端口访问本端口(3001)
res.send("Hello world from CROS.?");
});
- Request headers\ Response headers
Access-Control-Allow-Origin : 表示允许哪些原始域进行跨域访问,它的值要么是请求时 Origin字段的值,要么是一个 *,表示接受任意域名的请求
Access-Control-Allow-Credentials: 表示是否允许客户端发送 Cookie,是一个布尔值。默认情况下,Cookie不包括在 CORS 请求之中,设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器
Access-Control-Expose-Headers: CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段,自定义的header字段是拿不到的,如果想拿到自定义的Header 字段,就必须在 Access-Control-Expose-Headers里面指定
Access-Control-Request-Method: 用来列出浏览器的 CORS请求用到哪些HTTP方法 put\delete 等
SpringBoot Cors
- @CrossOrigin(origins = “*”)
@RestController
@CrossOrigin(origins = "*")
public class UserController {
@RequestMapping(value="/user/info")
//@CrossOrigin(origins = "*")
public String userInfo( ){
return "('natural')";
//局部方法上也可以添加@CrossOrigin 注解
}
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest reqs = (HttpServletRequest) servletRequest;
response.setHeader("Access-Control-Allow-Origin", reqs.getHeader("Origin"));
//直接给response header中添加 Access-Control-Allow-Origin
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Token");
filterChain.doFilter(servletRequest, servletResponse);
}
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//放行哪些原始域
.allowedOrigins("*")
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
.allowedHeaders("*");
}
}
总结CORS : Cors 是一个标准只要实现了对应的标准就可以做到跨域
- http://www.ruanyifeng.com/blog/2016/04/cors.html (阮一峰的跨域资源共享 CORS 详解)
window.postMessage
Iframe
- 每个iframe元素都有自己的会话历史记录(session history)和DOM树。包含嵌入内容的浏览上下文称为父级浏览上下文。顶级浏览上下文(没有父级)通常是由
Window
对象表示的浏览器窗口。 - 搞清楚window 指向谁。
- 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
- 一些可能嵌入跨域资源的标签
<script src="..."></script>
标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。<link rel="stylesheet" href="...">
标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type 消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera。<img>
嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,…@font-face
引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。<frame> 和 <iframe>
载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。- MDN: https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy (浏览器的同源策略)
window.postMessage();// 这里的windwos始终指向目标iframe
otherWindow.postMessage(message, targetOrigin, [transfer]);
window.addEventListener('message', receiveMessage, false);
receiveMessage = (event) => {}
event.origin //源
event.data
event.source // event.source.postMessage() 直接双向通信。
<body>
<div id="father">
<p>这里3000端口</p>
<input type="text"/>
<button>发送信息</button>
<p style="text-align: left;">message : <span></span></p>
</div>
<iframe id="child" src="http://localhost:3001"></iframe>
<script>
var input = document.getElementsByTagName('input')[0];
var span = document.getElementsByTagName('span')[0];
var btn = document.getElementsByTagName('button')[0];
var frame = document.getElementById('child').contentWindow;// otherwindows 顾名思义(目标窗口)
//var frame = window.frames['child2']
btn.onclick = function () {
var msg = input.value;
frame.postMessage('收到信息:' + msg + ' --from 3000 port!?', 'http://localhost:3001');
}
function receiveMessage (event) {
if (event.origin !== 'http://localhost:3001') {//防止来自其他origin的攻击
return false
}
var data = event.data;
span.innerHTML = data;
}
window.addEventListener('message', receiveMessage, false);//监听 message事件 调用receiveMessage 方法。
</script>
- 一个投机取巧比较low的办法,就是通过表单提交来进行登陆。
- 利用iframe 进行post跨域(隐藏的form表单提交)
const requestPost = ({url, data}) => {
// 首先创建一个用来发送数据的iframe.
const iframe = document.createElement('iframe')
iframe.name = 'iframePost'
iframe.style.display = 'none'
document.body.appendChild(iframe)
const form = document.createElement('form')
const node = document.createElement('input')
form.action = url
// 在指定的iframe中执行form
form.target = iframe.name
form.method = 'post'
for (let name in data) {
node.name = name
node.value = data[name].toString()
form.appendChild(node.cloneNode())
}
// 表单元素需要添加到主文档中.
form.style.display = 'none'
document.body.appendChild(form)
form.submit()
// 表单提交后,就可以删除这个表单,不影响下次的数据发送.
document.body.removeChild(form)
}
// 使用方式
requestPost({
url: 'http://localhost:3001/vbi/login.do',
data: {
msg: 'this is vbi'
}
})
以上部分例子是基于下面项目改的
https://github.com/FatDong1/cross-domain