跨域问题
跨域是指从一个域名的网页去请求另一个域名的资源, 比如当前在百度页面(https://baidu.com)去请求京东服务器(https://www.jd.com)的资源
传统请求不会跨域
在a站点
可以通过超链接
或者form表单
提交或者window.location.href
的方式跨域访问b站点
的资源(静态或者动态)
- 传统方式本质都是在地址栏上发了一次请求直接进行页面跳转,并不是从当前页面获取另一个页面的资源
<!--通过超链接的方式发送请求可以跨域-->
<a href="http://localhost:8081/b/index.html">跨域访问b站点的index页面</a>
<br>
<!--通过form表单的方式发送请求可以跨域-->
<form action="http://localhost:8081/b/user/reg" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="注册">
</form>
<br>
<!--通过js代码中的window.location.href/document.location.href的方式发送请求可以跨域-->
<button onclick="window.location.href='http://localhost:8081/b/index.html'">跨域访问b站点的index页面</button>
<button onclick="document.location.href='http://localhost:8081/b/index.html'">跨域访问b站点的index页面</button>
<!--使用script标签的src属性可以跨域加载js文件-->
<script type="text/javascript" src="http://localhost:8081/b/my.js"></script>
<br>
<!--img的src属性也可以跨域加载其他站点的图片-->
<img src="http://localhost:8081/b/bd_logo.png" />
b站点
的静态资源index.html
页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b应用的index页面</title>
</head>
<body>
<h1>b应用的index页面</h1>
</body>
</html>
b站点
的动态资源UserRegServlet
@WebServlet("/user/reg")
public class UserRegServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 响应到前端
response.getWriter().print(username + ":" + password);
}
}
Ajax请求跨域问题
在a站点
的页面中发送Ajax请求访问b站点
的资源,此时发起的请求就是跨域请求
<script type="text/javascript">
// 使用ES6新特性:箭头函数
window.onload = () => {
document.getElementById("btn").onclick = () => {
// 1. 创建核心对象(ES6的新特性:var let const关键字都可以定义变量)
let xmlHttpRequest = new XMLHttpRequest();
// 2. 注册回调函数
xmlHttpRequest.onreadystatechange = () => {
if (xmlHttpRequest.readyState == 4) {
// 状态码在这个范围内都可以
if (xmlHttpRequest.status >= 200 && xmlHttpRequest.status < 300) {
document.getElementById("mydiv").innerHTML = xmlHttpRequest.responseText
}
}
}
// 3. 开启通道
xmlHttpRequest.open("GET", "http://localhost:8081/b/hello", true)
// 4. 发送请求
xmlHttpRequest.send()
}
}
</script>
<button id="btn">发送ajax跨域请求</button>
<div id="mydiv"></div>
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 响应
response.getWriter().print("hello ajax!!!");
}
}
由于浏览器的同源策略,浏览器会把服务器响应的数据拦截导致页面无法渲染服务器响应的数据
# chrome浏览器报错
Access to XMLHttpRequest at 'http://localhost:63110/system/dictionary/all' from origin 'http://localhost:8601' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
# firefox浏览器报错
已拦截跨源请求:同源策略禁止读取位于 http://localhost:63110/system/dictionary/all 的远程资源(原因:CORS 头缺少 'Access-Control-Allow-Origin')
AJAX跨域解决方案
有时候我们是需要使用ajax进行跨域访问的,比如某公司的A页面(a.bjpowernode.com)需要获取B页面(b.bjpowernode.com)的资源
同源策略(CORS policy)
同源策略是指一段脚本只能读取来自同一来源的窗口和文档的属性有利于保护网站信息)
- 同源就是
协议、域名、端口
都相同,即便两个不同的域名指向同一个ip地址也非同源 - 如果没有同源策略,当你在网银站点输入了账号密码,然后访问了一些不规矩的网站,那么这些网站就可以访问刚刚的网银站点获取你的账号密码
当从一个地址请求另一个地址时,如果协议,域名,端口号
都一致时表示同源,有一个不一致就是跨域请求
,此时浏览器会在请求头上添加origin
- 同源策略是
浏览器的一种安全机制
,浏览器基于同源策略去判断用户发起的是否为跨域请求,避免浏览器受到 XSS、CSRF 等攻击 - 跨域请求只发生在浏览器和服务器之间,服务器之间不存在跨域请求
- 跨域并不是请求发不出去,
请求能发出去并且服务端也能收到请求并正常返回结果
,只是响应的数据被浏览器拦截了,因为浏览器认为你发起的是跨域请求 - 跨域的时候浏览器不允许共享同一个
XMLHttpRequest
对象, 因为共享同一个XMLHttpRequest对象是不安全的
URL1 | URL2 | 是否同源 | 描述 |
---|---|---|---|
http://localhost:8080/a/index.html | http://localhost:8080/a/first | 同源 | 协议 域名 端口一致 |
http://localhost:8080/a/index.html | http://localhost:8080/b/first | 同源 | 协议 域名 端口一致 |
http://www.myweb.com:8080/a.js | https://www.myweb.com:8080/b.js | 不同源 | 协议不同 |
http://www.myweb.com:8080/a.js | http://www.myweb.com:8081/b.js | 不同源 | 端口不同 |
http://www.myweb.com/a.js | http://www.myweb2.com/b.js | 不同源 | 域名不同 |
http://www.myweb.com/a.js | http://crm.myweb.com/b.js | 不同源 | 子域名不同 |
CORS(响应头)方案
CORS(Cross-origin resource sharing)
即跨域资源共享,它允许浏览器向跨源服务器发出XMLHttpRequest请求从而克服了AJAX只能同源使用的限制
服务端向浏览器响应数据时添加响应头Access-Control-Allow-Origin
,即告诉浏览器允许哪些站点跨域访问资源,这样浏览器就会放行对应的站点
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8080"); // 允许某个站点
response.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有站点
jsonp方案
jsonp(json with padding)
表示其是带填充的json,类似ajax请求可以以完成局部刷新效果并且可以解决跨域问题,但是只支持GET请求,不支持POST请求
通过script标签的src属性
进行跨域请求,服务器响应的JS代码会在script标签内解析
- script标签的src属性可以跨域访问
静态的xxx.js资源文件
,也可以跨域请求一个动态的Java程序
,请求响应的结果都会在script标签内解析
<script type="text/javascript">
// 自定义函数
function sayHello(data){
alert("hello," + data.name)
}
</script>
<!---->
<script type="text/javascript" src="http://localhost:8081/b/jsonp?fun=sayHello">
// 服务器响应的JS代码会在script标签内解析
sayHello({"name" : "jackson"})
</script>
第一步: 页面打开的时候先不创建script
标签,当我们点击按钮
的时候再创建script元素并设置src属性
发送jsonp请求实现页面局部刷新效果
<!--<script type="text/javascript" src="http://localhost:8081/b/jsonp?fun=sayHello"></script>-->
<script type="text/javascript">
// 自定义的函数, 参数data是一个json对象
function sayHello(data){
document.getElementById("mydiv").innerHTML = data.username
}
window.onload = () => {
document.getElementById("btn").onclick = () => {
// 创建script元素对象
const htmlScriptElement = document.createElement("script");
// 设置script的type属性
htmlScriptElement.type = "text/javascript"
// 设置script的src属性
htmlScriptElement.src = "http://localhost:8081/b/jsonp?fun=sayHello"
// 将script对象添加到body标签中(这一步就是加载script)
document.getElementsByTagName("body")[0].appendChild(htmlScriptElement)
}
}
</script>
<button id="btn">jsonp解决跨域问题,达到ajax局部刷新的效果</button>
<div id="mydiv"></div>
第二步: 后端处理前端发起的jsonp请求并响应一段JS代码,后端响应的永远都是字符串
,但是浏览器接会自动将这个字符串当做一段JS代码在script标签内解析并执行
@WebServlet("/jsonp")
public class JSONPServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 向前端响应一段js代码
PrintWriter out = response.getWriter();
//响应一个JS内置的alert函数,浏览器解析后可以直接调用用
//out.print("alert(123)");
//直接响应一个程序员自定义的sayHello函数,参数是一个json对象
//out.print("sayHello({\"name\" : \"jackson\"})");
//动态获取URL中请求参数对应的函数名,方法参数是一个json对象
String fun = request.getParameter("fun");
out.print(fun + "({\"name\" : \"jackson\"})");
}
}
jQuery封装的jsonp
直接使用jQuery封装的jsonp , 底层原理不变
<!--引入官方的jQuery库是官网的-->
<script type="text/javascript" src="/ajax/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
// 这个函数不用你写,jQuery可以帮助你自动生成回调
function jQuery3600508253314856699_1655528968612(json){
// 会继续自动调用success的回调函数
}
// 自定义的函数
function sayHello(data){
$("#mydiv").html("欢迎你:" + data.username)
}
$(function(){
$("#btn").click(function(){
// 发送类似ajax的请求,本质上并不是一个ajax请求
$.ajax({
type : "GET", // jsonp请求只支持get请求
// 实际上发送的请求是/b/jsonp?callback=jQuery3600508253314856699_1655528968612&_=1655528968613(时间戳)
// callback是请求参数名,jQuery3600508253314856699_1655528968612是随机函数名
url : "http://localhost:8081/b/jsonp",//指定跨域的url
dataType : "jsonp", // 指定数据类型是jsonp形式
jsonp : "fun", // 指定具体的请求参数名(之前的fun), 默认的请求参数名是callback
jsonpCallback : "sayHello" // 指定自定义的回调函数名(之前的sayHello),默认jQuery会自动生成一个随机的回调函数
// jQuery自动生成的回调函数会自动调用success的回调函数
/*success : function(data){ // data是一个json对象用来接收服务器端响应的数据
$("#mydiv").html("欢迎你:" + data.username)
}*/
})
})
})
</script>
<button id="btn">jQuery库封装的jsonp</button>
<div id="mydiv"></div>
代理机制原理
本站点的页面访问当前站点的ProxyServlet
不会有跨域问题,然后通过ProxyServlet
发送GET/POST请求访问跨域的资源
- 第一种方案(代码繁琐): 使用
JDK内置的API(java.net.URL)
发送HTTP请求 - 第二种方案(推荐): 使用第三方的开源组件发送http请求,比如apache开源免费的
httpclient
组件
第一步: 在a站点
的页面中访问当前站点中的ProxyServlet
<script type="text/javascript">
// ES6当中的有一个新语法箭头函数
window.onload = () => {
document.getElementById("btn").onclick = () => {
// 1.创建核心对象
const xmlHttpRequest = new XMLHttpRequest();
// 2.注册回调函数
xmlHttpRequest.onreadystatechange = () => {
if (xmlHttpRequest.readyState == 4) {
// 这里也可以使用区间的方式,因为状态码是200~299都是正常响应结束
if (xmlHttpRequest.status >= 200 && xmlHttpRequest.status < 300) {
document.getElementById("mydiv").innerHTML = xmlHttpRequest.responseText
}
}
}
// 3.开启通道
xmlHttpRequest.open("GET", "/a/proxy", true)
// 4.发送请求
xmlHttpRequest.send()
}
}
</script>
<button id="btn">使用代理机制解决ajax跨域访问</button>
<div id="mydiv"></div>
第二步: 在a站点的ProxyServlet
中通过httpclient
组件发送GET请求去访问b站点的TargetServlet
b站点的TargetServlet
会将数据响应给a站点的ProxyServlet
,然后ProxyServlet再把数据响应给前端
@WebServlet("/proxy")
public class ProxyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 目标地址
HttpGet httpGet = new HttpGet("http://localhost:8081/b/target");
// 设置类型 "application/x-www-form-urlencoded" "application/json"
httpGet.setHeader("Content-Type", "application/x-www-form-urlencoded");
//System.out.println("调用URL: " + httpGet.getURI());
// httpClient实例化
CloseableHttpClient httpClient = HttpClients.createDefault();
// 发起请求访问b站点的TargetServlet
HttpResponse resp = httpClient.execute(httpGet);
// 获取TargetServlet响应给ProxyServlet的数据
HttpEntity entity = resp.getEntity();
System.out.println("返回状态码:" + response.getStatusLine());
// 显示TargetServlet响应给ProxyServlet的数据
BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
String line = null;
StringBuffer responseSB = new StringBuffer();
while ((line = reader.readLine()) != null) {
responseSB.append(line);
}
//System.out.println("服务器响应的数据:" + responseSB);
reader.close();
httpClient.close();
// 将b站点的TargetServlet响应回来的数据继续响应给前端
response.getWriter().print(responseSB);
}
}
第三步: b站点的TargetServlet
,处理a站点ProxyServlet
发起的ajax请求
@WebServlet("/target")
public class TargetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 响应一个json字符串
response.getWriter().print("{\"username\":\"jackson\"}");
}
}
Nginx反向代理
代理服务器方案的实现原理: 同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略的
Nginx的反向代理也是使用了类似ProxyServlet
的代理机制来完成Ajax的跨域
- 第一步: 浏览器先访问Nginx提供的前端地址页面
http://192.168.101.10:8601
- 第二步: 前端页面发起的请求会先访问Nginx提供的一个同源地址(类似ProxyServlet)
http://192.168.101.11:8601/api/前端发起的具体请求
- 第三步: 由于服务端之间没有跨域,所以可以通过
ProxyServlet
实现跨域访问目标地址http://www.baidu.com:8601
其他常见方案
常见代理方案
- Node 中间件代理, postMesssage, websocket, window.name + iframe , location.hash + iframe, document.domain + iframe等
- vue-cli(Vue 脚手架自带的 8080 服务器也可以作为代理服务器,但是需要通过配置
vue.config.js
来启用这个代理