什么是浏览器跨域问题
- 跨域只存在于浏览器中,当A网站访问的资源不是同源的,就会产生跨域问题
- 同源策略是浏览器的一种安全机制,要求协议、域名、端口一致
- 这是前后端分离后的一个热门话题,在单体应用年代,程序员很少接触这个问题,但浏览器一直有这个安全机制
- localhost和127.0.0.1都指向本机,但也不同源,也就是说,要dns的域名一样,ip一样也不行(why?)
- js引用脚本是可以跨域访问的,如
<script />、<img />
为什么要限制跨域访问
浏览器同源策略包括AJAX同源策略、DOM同源策略
- AJAX同源策略主要用来防止CSRF攻击。如果没有AJAX同源策略,相当危险,我们发起的每一次HTTP请求都会带上请求地址对应的cookie,那么可以做如下攻击:
- 用户访问银行网站http://bank.com,并登录后,http://bank会向cookie中添加用户标记,为了后续的访问不用每次都登录
- 用户访问恶意网站http://evil.com,如果可以跨域访问,http://evil.com会用ajax发起对http?/bank.com的请求,如转账等!
- 因为请求时,浏览器默认会把http://bank.com对应的cookie发送过去,这样,http://bank.com会拿到之前的cookie,提取用户标志,验证用户无误,认为这个是本人的合法操作
- cookie 是明文的,不法网站一样可以利用,如何保证安全?
- DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:
- 做一个假网站,里面用iframe嵌套一个银行网站 http://mybank.com。
- 把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
- 这时如果用户输入账号密码,我们的主网站可以跨域访问到http://mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。
- 有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进 www.yinghang.com 改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。 2. 睡眼朦胧的你没看清楚,平时访问的银行网站是 www.yinhang.com,而现在访问的是 www.yinghang.com,这个钓鱼网站做了什么呢?
// HTML
<iframe ></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
- 同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。
如何进行跨域访问
推荐4和5
- jsonp,只支持get,不支持post,需要调用前端和被调用后端配合
- 后端HttpClient进行转发,两次请求,效率低,安全(类似Nginx反向代理)
- 设置响应头,允许跨域,适于小公司快速解决问题
- Nginx搭建API接口网关
- Zuul搭建API接口网关
1. jsonp,只支持get,不支持post,需要调用前端和被调用后端配合
1.1 使用《script src=""》来完成一个跨域请求
当点击"跨域获取数据"的按钮时,添加一个<script>
标签,用于发起跨域请求;注意看请求地址后面带了一个callback=showData的参数;
showData即是回调函数名称,传到后台,用于包裹数据。数据返回到前端后,就是showData(result)的形式,因为是script脚本,所以自动调用showData函数,而result就是showData的参数。
至此,我们算是跨域把数据请求回来了,但是比较麻烦,需要自己写脚本发起请求,然后写个回调函数处理数据,不是很方便。
<%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>跨域测试</title>
<script src="js/jquery-1.7.2.js"></script>
<script>
//回调函数
function showData (result) {
var data = JSON.stringify(result); //json对象转成字符串
$("#text").val(data);
}
$(document).ready(function () {
$("#btn").click(function () {
//向头部输入一个脚本,该脚本发起一个跨域请求
$("head").append("<script src='http://localhost:9090/student?callback=showData'><\/script>");
});
});
</script>
</head>
<body>
<input id="btn" type="button" value="跨域获取数据" />
<textarea id="text" style="width: 400px; height: 100px;"></textarea>
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//数据
List<Student> studentList = getStudentList();
JSONArray jsonArray = JSONArray.fromObject(studentList);
String result = jsonArray.toString();
//前端传过来的回调函数名称
String callback = request.getParameter("callback");
//用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
result = callback + "(" + result + ")";
response.getWriter().write(result);
}
1.2 再来看jquery的jsonp方式跨域请求
服务端代码不变,js代码如下:最简单的方式,只需配置一个dataType:‘jsonp’,就可以发起一个跨域请求。jsonp指定服务器返回的数据类型为jsonp格式,可以看发起的请求路径,自动带了一个callback=xxx,xxx是jquery随机生成的一个回调函数名称。
这里的success就跟上面的showData一样,如果有success函数则默认success()作为回调函数。
<%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>跨域测试</title>
<script src="js/jquery-1.7.2.js"></script>
<script>
$(document).ready(function () {
$("#btn").click(function () {
$.ajax({
url: "http://localhost:9090/student",
type: "GET",
dataType: "jsonp", //指定服务器返回的数据类型
success: function (data) {
var result = JSON.stringify(data); //json对象转成字符串
$("#text").val(result);
}
});
});
});
</script>
</head>
<body>
<input id="btn" type="button" value="跨域获取数据" />
<textarea id="text" style="width: 400px; height: 100px;"></textarea>
</body>
</html>
1.3 再看看如何指定特定的回调函数:第30行代码
回调函数你可以写到<script>
下(默认属于window对象),或者指明写到window对象里,看jquery源码,可以看到jsonp调用回调函数时,是调用的window.callback。
然后看调用结果,发现,请求时带的参数是:callback=showData;调用回调函数的时候,先调用了指定的showData,然后再调用了success。所以,success是返回成功后必定会调用的函数,就看你怎么写了。
<%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>跨域测试</title>
<script src="js/jquery-1.7.2.js"></script>
<script>
function showData (data) {
console.info("调用showData");
var result = JSON.stringify(data);
$("#text").val(result);
}
$(document).ready(function () {
// window.showData = function (data) {
// console.info("调用showData");
//
// var result = JSON.stringify(data);
// $("#text").val(result);
// }
$("#btn").click(function () {
$.ajax({
url: "http://localhost:9090/student",
type: "GET",
dataType: "jsonp", //指定服务器返回的数据类型
jsonpCallback: "showData", //指定回调函数名称
success: function (data) {
console.info("调用success");
}
});
});
});
</script>
</head>
<body>
<input id="btn" type="button" value="跨域获取数据" />
<textarea id="text" style="width: 400px; height: 100px;"></textarea>
</body>
</html>
1.4 再看看如何改变callback这个名称:第23行代码
指定callback这个名称后,后台也需要跟着更改。
<%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>跨域测试</title>
<script src="js/jquery-1.7.2.js"></script>
<script>
function showData (data) {
console.info("调用showData");
var result = JSON.stringify(data);
$("#text").val(result);
}
$(document).ready(function () {
$("#btn").click(function () {
$.ajax({
url: "http://localhost:9090/student",
type: "GET",
dataType: "jsonp", //指定服务器返回的数据类型
jsonp: "theFunction", //指定参数名称
jsonpCallback: "showData", //指定回调函数名称
success: function (data) {
console.info("调用success");
}
});
});
});
</script>
</head>
<body>
<input id="btn" type="button" value="跨域获取数据" />
<textarea id="text" style="width: 400px; height: 100px;"></textarea>
</body>
</html>
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//数据
List<Student> studentList = getStudentList();
JSONArray jsonArray = JSONArray.fromObject(studentList);
String result = jsonArray.toString();
//前端传过来的回调函数名称
String callback = request.getParameter("theFunction");
//用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
result = callback + "(" + result + ")";
response.getWriter().write(result);
}
1.5 最后看看jsonp是否支持POST方式:ajax请求指定POST方式
可以看到,jsonp方式不支持POST方式跨域请求,就算指定成POST方式,会自动转为GET方式;而后端如果设置成POST方式了,那就请求不了了。
jsonp的实现方式其实就是<script>
脚本请求地址的方式一样,只是ajax的jsonp对其做了封装,所以可想而知,jsonp是不支持POST方式的。
2. 后端HttpClient进行转发,两次请求,效率低,安全(类似Nginx反向代理)
@WebServlet("/getServlet")
public class ServletTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/**
* 创建请求
*/
CloseableHttpClient aDefault = HttpClients.createDefault();
/**
* 创建get请求
*/
HttpGet httpGet = new HttpGet("http://127.0.0.1/app.ZMTManage/?m=zmtmanage&c=index&a=init");
/**
* 执行请求
*/
CloseableHttpResponse execute = aDefault.execute(httpGet);
/**
* 获取状态
*/
int statusCode = execute.getStatusLine().getStatusCode();
if (200==statusCode){
/**
* 解析请求头
*/
String entity = EntityUtils.toString(execute.getEntity());
/**
* 写入response
*/
resp.getWriter().print(entity);
/**
* 关闭请求
*/
execute.close();
aDefault.close();
}
}
}
3. 设置响应头,允许跨域,适于小公司快速解决问题
@RequestMapping("/origin")
public String setHeader(HttpServletResponse response) {
// * 表示允许任何域名跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
// 指定特定域名可以访问
response.setHeader("Access-Control-Allow-Origin", "http:localhost:8080/");
// 设置其中一个即可
// 如果网关或其它地方设置了,1. 在网关过滤,不传递到下下游服务;2. 下游服务设置response.reset()
return "success";
}
4. Nginx搭建API接口网关(推荐)
- 这种方式不方便,是
5. Zuul搭建API接口网关(推荐)
服务器接收到请求,并处理了,只是浏览器不接收response?