各位观众老爷,大家好!我是你们的浏览器同源策略老司机,今天咱们就来聊聊这浏览器安全界的“门神”——同源策略 (Same-Origin Policy)。这玩意儿听起来高大上,但其实就是浏览器为了保护咱们的隐私,防止恶意网站偷窥咱们的个人信息搞出来的规矩。
一、啥是同源?同源策略又是啥?
想象一下,你家小区门口有个保安大爷,他要核实来访者是不是你家亲戚朋友,才能放他们进来。同源策略就扮演着类似保安大爷的角色。
那啥是“同源”呢? 简单来说,两个网页的协议 (protocol)、域名 (domain) 和端口 (port) 都相同,就可以认为是同源的。 缺一不可!
元素 | 举例 |
---|---|
协议 (Protocol) | http, https |
域名 (Domain) | example.com, sub.example.com |
端口 (Port) | 80, 443, 8080 |
比如:
http://www.example.com/index.html
和http://www.example.com/script.js
-> 同源http://www.example.com/index.html
和https://www.example.com/index.html
-> 不同源 (协议不同)http://www.example.com/index.html
和http://sub.example.com/index.html
-> 不同源 (域名不同)http://www.example.com:80/index.html
和http://www.example.com:8080/index.html
-> 不同源 (端口不同)
而同源策略就是浏览器的一项安全措施,它限制了一个源的文档或脚本如何才能去访问另一个源的资源。 也就是说,浏览器会阻止跨源的读写操作。 换句话说,如果两个页面不同源,那么A页面的Javascript就不能随意读取B页面里的数据,也不能随意操控B页面的DOM。
二、同源策略的具体限制
同源策略主要限制以下三种行为:
- Cookie、LocalStorage 和 IndexDB 无法跨域读取。 除非你设置了 Cookie 的
domain
属性允许跨子域访问。 - DOM 无法跨域操作。 A 页面里的 JS 没法直接修改 B 页面的 DOM 结构。
- AJAX 请求受到限制。 这就是我们最常遇到的跨域问题。 A 页面里的 JS 没法直接用 AJAX 向 B 页面发起请求。
三、同源策略的安全意义
为啥要有同源策略? 没了它会咋样?
想象一下,你同时登录了你的银行网站 bank.com
和一个看起来很不正经的网站 evil.com
。 如果没有同源策略,evil.com
上的恶意脚本就可以通过 AJAX 访问 bank.com
的 API,窃取你的银行账户信息,甚至直接帮你转账!
同源策略就像一道防火墙,阻止了这种恶意行为的发生,保护了我们的隐私和安全。
四、突破同源策略的常见方法
虽然同源策略很重要,但有时候我们又不得不进行跨域操作。 比如,你的网站需要从另一个域名下的 API 获取数据。 这时候,我们就需要一些“曲线救国”的方法来突破同源策略的限制。
下面介绍几种常见的跨域解决方案:
-
CORS (Cross-Origin Resource Sharing)
CORS 是目前最主流、最推荐的跨域解决方案。 它的核心思想是在服务器端设置 HTTP 响应头,告诉浏览器允许哪些源的请求访问该资源。
-
工作原理:
浏览器在发起跨域请求时,会在请求头中添加一个
Origin
字段,表明请求来自哪个源。 服务器收到请求后,会根据Origin
字段判断是否允许该请求。 如果允许,服务器会在响应头中添加Access-Control-Allow-Origin
字段,指定允许的源。 浏览器收到响应后,会检查Access-Control-Allow-Origin
字段,如果该字段包含当前源,或者设置为*
(表示允许所有源),那么浏览器就允许该请求。 否则,浏览器会阻止该请求。 -
简单请求 vs 预检请求
CORS 分为简单请求和预检请求。
-
简单请求: 满足以下所有条件的请求被称为简单请求:
- 请求方法是
GET
、HEAD
或POST
。 - HTTP 头信息不超过以下字段:
Accept
、Accept-Language
、Content-Language
、Content-Type
(只限于application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)。
对于简单请求,浏览器会直接发起跨域请求,并在请求头中添加
Origin
字段。 - 请求方法是
-
预检请求: 不满足简单请求条件的请求被称为预检请求。 比如,使用了
PUT
或DELETE
方法,或者Content-Type
设置为application/json
。对于预检请求,浏览器会先发起一个
OPTIONS
请求,询问服务器是否允许该跨域请求。OPTIONS
请求的请求头中包含Origin
字段,以及Access-Control-Request-Method
(表示实际请求的方法) 和Access-Control-Request-Headers
(表示实际请求的头信息) 字段。服务器收到
OPTIONS
请求后,会根据这些字段判断是否允许该请求。 如果允许,服务器会在响应头中添加以下字段:Access-Control-Allow-Origin
: 指定允许的源。Access-Control-Allow-Methods
: 指定允许的 HTTP 方法。Access-Control-Allow-Headers
: 指定允许的 HTTP 头信息。Access-Control-Max-Age
: 指定预检请求的有效期 (单位为秒)。
浏览器收到
OPTIONS
响应后,会检查这些字段,如果允许该跨域请求,才会发起实际的请求。
-
-
服务器端配置示例 (Node.js + Express):
const express = require('express'); const cors = require('cors'); // 引入 cors 中间件 const app = express(); // 允许所有来源的跨域请求 app.use(cors()); // 或者,更精确地控制允许的来源 // const corsOptions = { // origin: 'http://www.example.com' // 允许 example.com 的请求 // }; // app.use(cors(corsOptions)); app.get('/data', (req, res) => { res.json({ message: 'Hello from the server!' }); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
-
服务器端配置示例 (Java + Spring Boot):
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 允许所有路径 .allowedOrigins("http://www.example.com") // 允许 example.com .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的方法 .allowedHeaders("*") // 允许的头 .allowCredentials(true) // 是否允许发送 Cookie .maxAge(3600); // 预检请求的有效期 } }
-
优点:
- 标准化的解决方案,被所有现代浏览器支持。
- 灵活性高,可以精确控制允许的源、方法和头信息。
- 安全性高,服务器端可以对请求进行验证。
-
缺点:
- 需要服务器端配合,修改响应头。
- 对于不支持 CORS 的老旧浏览器,无法使用。
-
-
JSONP (JSON with Padding)
JSONP 是一种比较老的跨域解决方案,它利用了
<script>
标签可以跨域加载资源的特性。-
工作原理:
客户端定义一个回调函数,然后通过
<script>
标签向服务器发起请求。 服务器端将数据包装在一个函数调用中,并将该函数调用返回给客户端。 客户端收到响应后,会执行该函数调用,从而获取到数据。 -
客户端代码示例:
<!DOCTYPE html> <html> <head> <title>JSONP Example</title> </head> <body> <script> function handleResponse(data) { console.log('Received data:', data); // 处理数据 } function loadData() { var script = document.createElement('script'); script.src = 'http://api.example.com/data?callback=handleResponse'; // 注意这里的 callback 参数 document.body.appendChild(script); } loadData(); </script> </body> </html>
-
服务器端代码示例 (Node.js + Express):
const express = require('express'); const app = express(); app.get('/data', (req, res) => { const callback = req.query.callback; const data = { message: 'Hello from the server!' }; const jsonp = `${callback}(${JSON.stringify(data)})`; // 构造 JSONP 响应 res.type('application/javascript'); // 设置 Content-Type res.send(jsonp); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
-
优点:
- 兼容性好,支持老旧浏览器。
- 简单易用,客户端和服务端代码都比较简单。
-
缺点:
- 只支持
GET
请求。 - 安全性较差,容易受到 XSS 攻击 (如果服务器端没有对数据进行严格的过滤)。
- 需要服务器端配合,修改响应格式。
- 只支持
-
-
Proxy (代理)
Proxy 是一种比较通用的跨域解决方案。 它的核心思想是在服务器端设置一个代理服务器,客户端先向代理服务器发起请求,然后代理服务器再向目标服务器发起请求,并将结果返回给客户端。
-
工作原理:
由于代理服务器和客户端是同源的,所以客户端可以毫无阻碍地向代理服务器发起请求。 而代理服务器和目标服务器之间的通信,不受同源策略的限制,因为这是服务器之间的通信,不是浏览器行为。
-
实现方式:
- 反向代理: 在服务器端使用 Nginx 或 Apache 等 Web 服务器配置反向代理。
- 中间层代理: 使用 Node.js 等后端技术搭建一个中间层服务器,作为代理。
-
反向代理配置示例 (Nginx):
server { listen 80; server_name www.example.com; // 你的域名 location /api/ { // /api/ 开头的请求会被代理 proxy_pass http://api.another-domain.com/; // 目标服务器地址 proxy_set_header Host $host; // 传递 Host 头 proxy_set_header X-Real-IP $remote_addr; // 传递客户端 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; // 传递 X-Forwarded-For 头 } }
客户端代码只需要访问
http://www.example.com/api/xxx
即可,Nginx 会将请求转发到http://api.another-domain.com/xxx
。 -
中间层代理代码示例 (Node.js + Express +
http-proxy-middleware
):const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); // 引入中间件 const app = express(); app.use('/api', createProxyMiddleware({ target: 'http://api.another-domain.com', // 目标服务器地址 changeOrigin: true, // 必须设置,否则会报错 pathRewrite: { // 可选:重写路径 '^/api': '' // 将 /api 替换为空字符串 }, })); app.listen(3000, () => { console.log('Proxy server listening on port 3000'); });
客户端代码只需要访问
http://localhost:3000/api/xxx
即可,该代理服务器会将请求转发到http://api.another-domain.com/xxx
。 -
优点:
- 客户端代码无需修改。
- 可以解决各种跨域问题,包括 Cookie、LocalStorage 等。
- 安全性较高,可以在代理服务器端对请求进行验证和过滤。
-
缺点:
- 需要搭建代理服务器,增加服务器端的负担。
- 可能会增加网络延迟。
-
五、一些补充说明
-
document.domain
: 只适用于二级域名相同的情况。 比如,a.example.com
和b.example.com
可以通过设置document.domain = 'example.com'
来实现跨域。 但这种方法已经逐渐被淘汰,不推荐使用。 -
window.postMessage
: 允许不同源的页面之间进行通信。 但需要双方都监听message
事件,并对消息进行验证,以防止恶意代码利用。 安全性较高,但实现起来比较繁琐。 -
iframe 跨域: 可以使用
postMessage
或者 URL 的 hash 值进行通信。
六、总结
同源策略是浏览器安全的重要基石,它有效地保护了我们的隐私和安全。 但在实际开发中,我们常常需要进行跨域操作。 CORS 是目前最主流、最推荐的跨域解决方案。 JSONP 适用于老旧浏览器,但安全性较差。 Proxy 是一种通用的解决方案,但需要搭建代理服务器。
选择哪种跨域解决方案,取决于具体的场景和需求。 在选择方案时,需要权衡安全性、兼容性和易用性。
好了,今天的讲座就到这里。 希望大家对浏览器同源策略有了更深入的了解。 如果还有什么疑问,欢迎提问! 咱们下期再见!