iframe 在同源和跨域的通信方式
同源
父页面和 iframe 可以直接访问对方的 DOM 和 JavaScript 对象。
这种方式简单高效,但需要注意代码的安全性和健壮性,避免产生错误或安全漏洞。
跨域
由于同源策略的限制,需要使用 window.postMessage() 方法进行通信。
同源情况下的通信
直接访问 iframe 的内容
从父页面访问 iframe
父页面可以通过 iframe 元素的 contentWindow 或 contentDocument 属性获取 iframe 的 window 对象或 document 对象,然后直接访问其 DOM 或 JavaScript 变量。
从 iframe 访问父页面
iframe 可以通过 window.parent 访问父页面的 window 对象。
示例
父页面(parent.html)
<!DOCTYPE html>
<html>
<head>
<title>父页面</title>
</head>
<body>
<h1>父页面</h1>
<iframe id="myIframe" src="iframe.html"></iframe>
<script>
// 等待 iframe 加载完成
document.getElementById('myIframe').onload = function() {
// 获取 iframe 的 window 对象
let iframeWindow = document.getElementById('myIframe').contentWindow;
// 调用 iframe 中的函数
iframeWindow.showMessageFromIframe();
// 访问 iframe 中的变量
console.log('从 iframe 获取的变量:', iframeWindow.iframeVariable);
// 修改 iframe 中的 DOM 元素
let iframeDocument = iframeWindow.document;
iframeDocument.getElementById('iframeElement').innerText = '父页面修改了 iframe 的内容';
// 从 iframe 获取输入框的值
let inputValue = iframeDocument.getElementById('iframeInput').value;
console.log('从 iframe 获取的输入框值:', inputValue);
};
// 定义供 iframe 调用的函数
function showMessageFromParent() {
alert('这是父页面的消息!');
}
</script>
</body>
</html>
iframe 页面(iframe.html)
<!DOCTYPE html>
<html>
<head>
<title>iframe 页面</title>
</head>
<body>
<h2>iframe 页面</h2>
<p id="iframeElement">这是 iframe 中的段落。</p>
<input type="text" id="iframeInput" value="iframe 输入框的默认值">
<script>
// 定义变量
let iframeVariable = '这是 iframe 中的变量';
// 定义函数
function showMessageFromIframe() {
alert('这是 iframe 中的消息!');
}
// 调用父页面的函数
window.parent.showMessageFromParent();
// 访问父页面的 DOM 元素
let parentTitle = window.parent.document.title;
console.log('父页面的标题是:', parentTitle);
// 修改父页面的背景颜色
window.parent.document.body.style.backgroundColor = '#f0f8ff';
</script>
</body>
</html>
确保 iframe 已经加载完成,才能访问其内容。
跨域情况下的通信
当父页面和 iframe 来自不同的域名、协议或端口时,浏览器的同源策略将阻止它们直接访问对方的内容。
使用 postMessage 进行跨域通信
window.postMessage() 方法允许来自不同源的窗口之间安全地进行通信。
示例
父页面(parent.html)
<!DOCTYPE html>
<html>
<head>
<title>父页面</title>
</head>
<body>
<h1>父页面</h1>
<iframe id="myIframe" src="https://aichihongdoudemao.com/iframe.html"></iframe>
<script>
let iframe = document.getElementById('myIframe');
// 向 iframe 发送消息
iframe.onload = function() {
let message = { type: 'GREETING', text: '你好,iframe!' };
iframe.contentWindow.postMessage(message, 'https://otherdomain.com');
};
// 接收来自 iframe 的消息
window.addEventListener('message', function(event) {
// 验证消息来源
if (event.origin !== 'https://aichihongdoudemao.com/iframe.html') {
console.warn('来自未知来源的消息:', event.origin);
return;
}
// 处理消息
console.log('收到来自 iframe 的消息:', event.data);
});
</script>
</body>
</html>
iframe 页面(iframe.html)
<!DOCTYPE html>
<html>
<head>
<title>iframe 页面</title>
</head>
<body>
<h2>iframe 页面</h2>
<script>
// 接收来自父页面的消息
window.addEventListener('message', function(event) {
// 验证消息来源
if (event.origin !== 'https://aichihongdoudemao.com/iframe.html') {
console.warn('来自未知来源的消息:', event.origin);
return;
}
// 处理消息
console.log('收到来自父页面的消息:', event.data);
// 回复父页面
let reply = { type: 'RESPONSE', text: '你好,父页面!' };
event.source.postMessage(reply, event.origin);
});
</script>
</body>
</html>
在 postMessage 中指定准确的 targetOrigin,不要使用 "*",以防止消息被发送到不受信任的窗口。
最佳实践和安全考虑
明确的 targetOrigin:始终在 postMessage 中指定准确的目标源,防止数据泄露。
// 不推荐的做法
iframe.contentWindow.postMessage(message, '*'); // 可能导致安全风险
// 推荐的做法
iframe.contentWindow.postMessage(message, 'https://otherdomain.com');
验证 event.origin:在接收消息时,验证消息的来源。
window.addEventListener('message', function(event) {
if (event.origin !== 'https://expected-origin.com') {
console.warn('不可信的消息来源,已忽略。');
return;
}
// 处理可信的消息
});
验证消息的数据结构和内容:确保消息的数据格式符合预期,防止被恶意代码利用。
if (typeof event.data !== 'object' || !event.data.type) {
console.warn('消息格式不正确,已忽略。');
return;
}
CORS 跨域原理
CORS 跨域的原理实际上是浏览器与服务器通过一些 HTTP 协议头来做一些约定和限制。可以查看 HTTP-访问控制(CORS)
IE 上跨域访问没有权限
在跨域发送 ajax 请求时提示没有权限。 因为IE浏览器默认对跨域访问有限制。需要在浏览器设置中去除限制。 方法: 设置 > Internet 选项 > 安全 > 自定义级别 > 在设置中找到其他 - 在【其他】中将【通过域访问数据源】启用。
如何实现 CORS?
客户端处理
客户端可以向远程服务器发送签名请求。
如下示例代码:在 CORS 请求中以 Authorization 标头的形式发送凭据:
function sendAuthRequestToCrossOrigin() { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("demo").innerHTML = this.responseText; } }; xhr.open('GET', "https://yuanjava:8000/categories", true); xhr.setRequestHeader('Authorization', 'Bearer rtikkjhgffw456tfdd'); xhr.withCredentials = true; xhr.send(); }
服务器端处理
方法1:直接采用 SpringBoot 的注解 @CrossOrigin
如下示例代码如下,可以把 @CrossOrigin 加在每个 Controller 上,也可以加在它们的公共父类上:
@CrossOrigin @RestController public class TestController extends BaseController { // 其他逻辑 }
方法2: 采用过滤器(filter)的方式
如下示例代码:增加一个 CORSFilter 类,并实现 Filter 接口即可。
`@Component public class CORSFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse res = (HttpServletResponse) response; res.addHeader("Access-Control-Allow-Credentials", "true"); res.addHeader("Access-Control-Allow-Origin", "*"); res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN"); if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) { response.getWriter().println("ok"); return; } chain.doFilter(request, response); } @Override public void destroy() { } @Override public void init(FilterConfig filterConfig) throws ServletException { } }`
方法3: 配置 Configuration
如下示例代码:增加一个配置类继承 WebMvcConfigurerAdapter 或者实现 WebMvcConfigurer 接口,项目启动时,会自动读取配置。
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class CorsConfig extends WebMvcConfigurerAdapter { static final String ORIGINS[] = new String[]{"GET", "POST", "PUT", "DELETE"}; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods(ORIGINS).maxAge(3600); } }
另外,在服务器,可以通过设置响应头部来细粒度配置 CORS,具体的如下:
1.允许所有源访问
HTTP/1.1 200 OK Access-Control-Allow-Origin: *
2.允许特定源访问
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com
3.允许凭据请求访问
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com Access-Control-Allow-Credentials: true
4.允许特定方法和头部
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization
5.设置预检请求的缓存时间
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 3600 // 3600秒
通常来说,在服务器解决 CORS是一种比较常见和彻底的方式,我们可以在服务器灵活的设置允许跨域访问的域名或者地址。
7. 常见问题及解决方案
问题1:No ‘Access-Control-Allow-Origin’ header is present on the requested resource
问题描述:当浏览器发起跨域请求时,未在响应中找到 Access-Control-Allow-Origin 头部。
解决方案:确保服务器端正确设置了 Access-Control-Allow-Origin 头部。例如:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com
问题2:The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’
问题描述:当请求包含凭据时,Access-Control-Allow-Origin 头部不能设置为通配符 *。
解决方案:明确指定允许的源,并确保设置了 Access-Control-Allow-Credentials 头部。例如:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://yuanjava.com Access-Control-Allow-Credentials: true
问题3:CORS preflight channel did not succeed
问题描述:预检请求失败,可能是由于服务器未正确处理 OPTIONS 请求。
解决方案:确保服务器正确处理 OPTIONS 请求并返回相应的 CORS 头部。例如,在 Node.js/Express 中:
app.options('/api/data', (req, res) => { res.header('Access-Control-Allow-Origin', 'https://yuanjava.com'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.header('Access-Control-Allow-Credentials', 'true'); res.sendStatus(204); });
8. 总结
CORS 是现代 Web 开发中不可或缺的机制,它允许 Web 应用在安全的前提下进行跨域资源请求,通过理解 CORS 的工作原理和配置方法,可以帮助我们有效地解决跨域请求的问题。
JSONP 跨域实现
- JSONP:出现的早,兼容性好(兼容低版本IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持
GET请求,不支持POST请求。 - CORS:出现的较晚,它是 W3C 标准,属于跨域
AJAX请求的根本解决方案。支持GET和POST请求。缺点是不兼容某些低版本的浏览器。 - Nginx反向代理:同源策略对服务器不加限制,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持
session,不需要修改任何代码,并且不会影响服务器性能。
二、JSONP 概述
JSONP (JSON with Padding) 是 JSON 的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。
2.1 JSONP原理
事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。
2.2 优点
- 它不像
XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制 - 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要
XMLHttpRequest或ActiveX的支持 - 并且在请求完毕后可以通过调用
callback的方式回传结果
2.3 缺点
- 它只支持
GET请求而不支持POST等其它类型的 HTTP 请求 - 它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript 调用的问题
三、JSONP 应用流程
-
设定一个
script标签<script src="http://jsonp.js?callback=cb"></script> // 或 let script = document.createElement('script'); script.src = "http://jsonp.js?callback=cb"; body.append(script) callback定义了一个函数名,而远程服务端通过调用指定的函数并传入参数来实现传递参数,将function(response)传递回客户端
router.get('/', function (req, res, next) {
(() => {
const data = {
x: 10
};
let params = req.query;
if (params.callback) {
let callback = params.callback;
console.log(params.callback);
res.send(`${callback}(${JSON.stringify(data.x)})`);
} else {
res.send('err');
}
})();
});
- 客户端接收到返回的 JS 脚本,开始解析和执行
function(response)
四、JSONP 实现
3.1 简单的实例:
一个简单的 JSONP 实现,其实就是拼接URL,然后将动态添加一个script元素到头部。
前端 JSONP 方法示例:
function jsonp(req) {
var script = document.createElement('script');
var url = req.url + '?callback=' + req.callback.name;
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
前端 JS 示例:
function hello(res){
alert('hello ' + res.data);
}
jsonp({
url : '',
callback : hello
});
服务器端代码:
var http = require('http');
var urllib = require('url');
var port = 8080;
var data = {'data':'world'};
http.createServer(function(req,res){
var params = urllib.parse(req.url,true);
if(params.query.callback){
console.log(params.query.callback);
// jsonp
var str = params.query.callback + '(' + JSON.stringify(data) + ')';
res.end(str);
} else {
res.end();
}
}).listen(port,function(){
console.log('jsonp server is on');
});
3.2 可靠的 JSONP 实例:
(function (global) {
var id = 0,
container = document.getElementsByTagName("head")[0];
function jsonp(options) {
if(!options || !options.url) return;
var scriptNode = document.createElement("script"),
data = options.data || {},
url = options.url,
callback = options.callback,
fnName = "jsonp" + id++;
// 添加回调函数
data["callback"] = fnName;
// 拼接url
var params = [];
for (var key in data) {
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
url = url.indexOf("?") > 0 ? (url + "&") : (url + "?");
url += params.join("&");
scriptNode.src = url;
// 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法
global[fnName] = function (ret) {
callback && callback(ret);
container.removeChild(scriptNode);
delete global[fnName];
}
// 出错处理
scriptNode.onerror = function () {
callback && callback({error:"error"});
container.removeChild(scriptNode);
global[fnName] && delete global[fnName];
}
scriptNode.type = "text/javascript";
container.appendChild(scriptNode)
}
global.jsonp = jsonp;
})(this);
使用示例:
jsonp({
url : "www.example.com",
data : {id : 1},
callback : function (ret) {
console.log(ret);
}
});
五、JSONP安全性问题
5.1 CSRF攻击
前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。
解决方法:验证JSONP的调用来源(Referer),服务端判断 Referer 是否是白名单,或者部署随机 Token 来防御。
5.2 XSS漏洞
不严谨的 content-type 导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://abc.com?callback=douniwan, 然后返回 douniwan({ data }),那假如请求 http://abc.com?callback=<script>alert(1)</script> 不就返回 <script>alert(1)</script>({ data })了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback 参数,直接当 HTML 解析了,就是一个赤裸裸的 XSS 了。
解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成<,>换成>)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。
5.3 服务器被黑,返回一串恶意执行的代码
可以将执行的代码转发到服务端进行校验 JSONP 内容校验,再返回校验结果。
748

被折叠的 条评论
为什么被折叠?



