同源策略那些事儿
重现我们先来写个用 ajax 提交表单的小小小的 demo,这毕竟太常见了。 /Test/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>index</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.open('post','http://127.0.0.1/Test/index.php',true); xhr.onreadystatechange = () => { if(xhr.readyState === 4 && xhr.status === 200) { document.write(xhr.responseText); } }; xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.send('username=yang'); </script> </body> </html> /Test/index.php <?php echo $_POST['username']; ?>
不出意外,你会得到以下大礼包: XMLHttpRequest cannot load http://127.0.0.1/Test/index.php. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. 此刻,有的人微微一笑,有的人一脸懵逼。 怎么回事?之所以会出现这个问题,是因为浏览器有同源策略 (Same-origin policy) 的限制,一个域 (origin) 的脚本,在未经允许的情况下,不得通过 DOM 读取另一个域的文档 (document) 的内容或属性。 同源策略在 Web 应用安全中扮演着重要的角色,它能保护一个网站的敏感信息,防止恶意脚本的窃取。同源策略中的
如果有一个地址为: http://example.com/test/a.html # 同源 https://example.com # 不同源,协议不同 http://www.example.com # 不同源,host 不同 http://example.com:8080 # 不同源,端口不同 规避同源策略的方法然而我们有些时候是需要在不同源的地址间进行通信的,有以下的方法可以用来规避同源策略。 document.domain
无码言*。我们来试试。 /Test/main.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Main window</title> </head> <body> <p>This is the main window</p> <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe> <script> document.domain = 'w1.localhost'; </script> </body> </html> /Test/iframe.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Iframe window</title> </head> <body> <p>This is the iframe window</p> <button id="btn">Close this iframe</button> <script> document.domain = 'w1.localhost'; const btn = document.querySelector('#btn'); const parent = window.parent.document; const frame = parent.querySelector('#child-iframe'); btn.onclick = () => { parent.body.removeChild(frame); }; </script> </body> </html>
访问 另外,我们可以通过在服务器中如下设置 <?php # 假设发起请求的域与此域是同域 # 指定了域名的话就相当于包含了所有子域名,所有子域名和此父域名都可以共享 cookie setcookie('username','Sam Yang',time() + 24 * 3600,'/',"w1.localhost"); # setcookie('user','/'); // 不指定则默认当前域名,cookie 不可被子域名共享 ?> 然后你在客户端的所有子域名下的页面都可以通过 跨文档消息传递 (cross-document messaging)
通过 HTML5 提供的 这个方法的主要语法是这样的: otherWindow.postMessage(message,targetOrigin)
接收数据的窗口可以监听
我们来用一下: /Test/main.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Main window</title> </head> <body> <p>This is the main window</p> <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe> <script> const iframe = document.querySelector('#child-iframe'); const iframeWin = iframe.contentWindow; // 获得 iframe 元素的窗口 iframe.onload = () => { // 等待 iframe 窗口完全加载完再发送消息 iframeWin.postMessage('hello,my friend','http://w3.w1.localhost'); }; </script> </body> </html> /Test/iframe.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Iframe window</title> </head> <body> <p>This is the iframe window</p> <script> window.onmessage = (event) => { if(event.origin !== 'http://w2.w1.localhost') return; document.write(event.data); }; </script> </body> </html> 访问
JSONP (JSONwith padding)
直译这个东东,就是“填充的 JSON”,这是跟 Ajax) 一样的老爷爷了,不同的是,前者要退休了。 这项技术之所以出现,是因为 /Test/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>index</title> </head> <body> <script> function addScriptTag(src) { const script = document.createElement('script'); script.src = src; document.body.appendChild(script); } window.onload = () => { addScriptTag('http://127.0.0.1/Test/index.php?callback=sayHello'); // 这里的 `callback` 换成其他名字也可以 }; function sayHello(data) { // 浏览器会自动解析得到的 JSON 数据,无需手动解析 document.body.append(`Hello,${data.username}`); } </script> </body> </html> /Test/index.php <?php $callback = $_GET['callback']; $data = array( 'username' => 'samyang' ); $result = json_encode($data); echo "{$callback}({$result})"; ?> 访问 当然啦,JSONP 是很容易遭到跨站请求伪造攻击的,所以你懂的。 WebSocket
WebSocket 是一种基于 这项技术仍然不稳定,但有一些成熟的相应实现,如 Socket.IO,有兴趣可以参见我写的 Socket.IO 的教程。 这项技术主要用于实时通信,此处不做进一步详述,想进一步探索,见 WebSocket。 跨域资源共享 (Cross-Origin Resource Sharing,CORS)好了,请出我们的大 boss。与 JSONP 的只支持 GET 请求相比,CORS 支持所有类型的 HTTP 请求。
ajax 受到同源策略的限制,使用 ajax 技术时,在未经允许的情况下,如果跨域请求发出给了服务器端并返回了数据 (视浏览器情况而定,有的在发出时即拦截),则客户端无法读取服务器端返回的数据。CORS 允许服务器端进行跨域访问控制,从而使跨域数据传输得以安全进行。 我们首先来了解下为了支持 CORS,增加了哪些 HTTP 首部字段。 HTTP 请求首部字段
OriginOrigin: <origin> 表明发起请求的源 (origin),这是一个 URI,不包含任何路径信息,只是服务器的名字。 Access-Control-Request-MethodAccess-Control-Request-Method: <method> 这个字段用于预检请求 (下文会讲),其作用是将实际请求所使用的 HTTP 方法告诉服务器。 Access-Control-Request-HeadersAccess-Control-Request-Headers: <field-name>[,<field-name>]* 这个字段用于预检请求,其作用是将实际请求所携带的自定义首部字段告诉服务器。 HTTP 响应首部字段Access-Control-Allow-OriginAccess-Control-Allow-Origin: <origin> | * 指定可以访问当前资源的外源 (origin),可以为不含路径信息的一个 URI,也可以是通配符 Access-Control-Expose-Headers用 XMLHttpRequest 对象的 getResponseHeader() 方法可以获取一些基本的响应头,当想要获取一些额外的响应头时,可以用这个字段指定。 Access-Control-Max-AgeAccess-Control-Max-Age: <delta-seconds> 指定预检请求的结果能被缓存多少秒。在这个缓存时间内,浏览器无须为同一请求再次发起预检请求。 Access-Control-Allow-MethodsAccess-Control-Allow-Methods: <method>[,<method>]* 这个字段用于预检请求响应,其指明了实际请求时所允许使用的 HTTP 方法。 Access-Control-Allow-HeadersAccess-Control-Allow-Headers: <field-name>[,<field-name>]* 这个字段用于预检请求响应,其指明了实际请求时允许携带的自定义首部字段。 Access-Control-Allow-Credentials讲这个字段前,我们先讲下 XMLHttpRequest 对象的 withCredentials 属性来用,withCredentials 设置为 Access-Control-Allow-Credentials 字段指定了当客户端设置了 withCredentials 为 当允许携带身份凭证时,请求头中包含了可能含有隐私信息的 cookie 数据,所以服务器端不得设置 Access-Control-Allow-Origin 的值为
简单请求 (simple request)不会触发 CORS 预检请求 (下文会提到) 的请求即为简单请求,具体来说,就是同时满足下列条件的请求:
/Test/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>index</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.open('get','http://w1.localhost/Test/index.php',true); xhr.send(); </script> </body> </html> /Test/index.php <?php header('Access-Control-Allow-Origin: *'); ?> 访问 # 请求头 GET /Test/index.php HTTP/1.1 Host: w1.localhost Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Origin: http://w2.localhost # 注意这个字段 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/60.0.3112.90 Safari/537.36 Accept: */* DNT: 1 Referer: http://w2.localhost/Test/main.html Accept-Encoding: gzip,deflate,br Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2 HT-Ver: 1.1.2 HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J # 响应头 HTTP/1.1 200 OK Date: ****** # 已打码 Server: Apache/2.4.25 (Unix) PHP/5.6.30 X-Powered-By: PHP/5.6.30 Access-Control-Allow-Origin: * # 注意这个字段 Content-Length: 0 Keep-Alive: timeout=5,max=100 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 根据上述条件,这是一个简单请求,我们使用 预检请求 (preflight request) 和 实际请求 (actual request)为了避免那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先先使用OPTIONS 方法发起一个预检请求,从而获知服务器端是否允许该跨域请求,获得允许之后才发起实际请求。满足下述任一条件时,即应首先发送预检请求:
/Test/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>index</title> </head> <body> <script> const xhr = new XMLHttpRequest(); const body = '<?xml version="1.0"?><person><name>Arun</name></person>'; xhr.open('post',true); xhr.setRequestHeader('X-PINGOTHER','pingpong'); xhr.setRequestHeader('Content-Type','application/xml'); xhr.send(body); </script> </body> </html> /Test/index.php <?php header('Access-Control-Allow-Origin: http://w2.localhost'); header('Access-Control-Allow-Headers: X-PINGOTHER,Content-Type'); header('Access-Control-Allow-Methods: POST,GET,OPTIONS'); header('Access-Control-Max-Age: 86400'); ?> 访问 # 请求头 OPTIONS /Test/index.php HTTP/1.1 Host: w1.localhost Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Access-Control-Request-Method: POST # 关注它 Origin: http://w2.localhost # 关注它 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/60.0.3112.90 Safari/537.36 Access-Control-Request-Headers: content-type,x-pingother # 关注它 Accept: */* DNT: 1 Referer: http://w2.localhost/Test/main.html Accept-Encoding: gzip,zh-TW;q=0.2 HT-Ver: 1.1.2 HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J # 响应头 HTTP/1.1 200 OK Date: ****** # 已打码 Server: Apache/2.4.25 (Unix) PHP/5.6.30 X-Powered-By: PHP/5.6.30 Access-Control-Allow-Origin: http://w2.localhost # 关注它 Access-Control-Allow-Headers: X-PINGOTHER,Content-Type # 关注它 Access-Control-Allow-Methods: POST,OPTIONS # 关注它 Access-Control-Max-Age: 86400 # 关注它 Content-Length: 0 Keep-Alive: timeout=5,max=100 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 预检请求之后的实际请求头和响应头: # 请求头 POST /Test/index.php HTTP/1.1 Host: w1.localhost Connection: keep-alive Content-Length: 55 Pragma: no-cache Cache-Control: no-cache X-PINGOTHER: pingpong # 关注它 Origin: http://w2.localhost User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/60.0.3112.90 Safari/537.36 Content-Type: application/xml # 关注它 Accept: */* DNT: 1 Referer: http://w2.localhost/Test/main.html Accept-Encoding: gzip,zh-TW;q=0.2 HT-Ver: 1.1.2 HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J # 响应头 HTTP/1.1 200 OK Date: ****** # 已打码 Server: Apache/2.4.25 (Unix) PHP/5.6.30 X-Powered-By: PHP/5.6.30 Access-Control-Allow-Origin: http://w2.localhost Access-Control-Allow-Headers: X-PINGOTHER,Content-Type Access-Control-Allow-Methods: POST,OPTIONS Access-Control-Max-Age: 86400 Content-Length: 0 Keep-Alive: timeout=5,max=99 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 一些小伎俩之所以叫小伎俩,是因为这里所讲的方法都只是在小数据量的跨域通信中比较方便,大数据量则不宜使用。
片段标识符 (fragment identifier)
片段标识符是网址 URL 中 /Test/main.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Main window</title> </head> <body> <p>This is the main window</p> <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe> </body> </html> /Test/iframe.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Iframe window</title> </head> <body> <p>This is the iframe window</p> <script> window.onhashchange = () => { document.write(`hello,${window.location.hash.substr(1)}`); }; </script> </body> </html> 访问 document.querySelector('#child-iframe').src += '#samyang' emmmm... 小窗口内容变了! 但是呢,这种方法是比较鸡肋的,虽然 查询字符串 (search string,aka,query string)
这是另一种用于小数据量跨域通信的方法,也是我所常用的方法,也是相比于前者更推荐的做法。查询字符串是 URL 中 /Test/main.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Main window</title> </head> <body> <p>This is the main window</p> <button>Click me</button> <script> document.querySelector('button').onclick = () => { location.href = `http://w3.w1.localhost/Test/iframe.html?username=samyang`; }; </script> </body> </html> /Test/iframe.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Iframe window</title> </head> <body> <p>This is the iframe window</p> <button id="btn">Close this iframe</button> <script> window.onload = () => { document.write(location.search.substr(1)) }; </script> </body> </html> 访问 window.name
这个属性是属于窗口的,只要窗口不变,即使页面变为不同域,这个属性的值也不变。 /Test/main.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Main window</title> </head> <body> <p>This is the main window</p> <script> window.name = 'samyang'; location.href = 'https://www.sogou.com/'; </script> </body> </html> 访问 结尾好了,大概是这么多了(其实还有很多),如有疏漏请指出。 Reference
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |