JSONP技术详解与实现方案
JSONP是一种巧妙的跨域解决方案,通过利用<script>标签不受同源策略限制的特性来实现跨域数据获取。优点兼容性好,支持老旧浏览器实现简单,不需要服务器特殊配置支持GET请求跨域缺点仅支持GET请求存在安全风险(XSS攻击)错误处理困难逐渐被CORS取代使用场景需要兼容老旧浏览器的项目第三方API只提供JSONP接口简单的跨域数据获取需求在现代Web开发中,建议优先使用CORS、代理服务器或p
JSONP技术详解与实现方案
JSONP(JSON with Padding)是一种跨域数据交互协议,它允许在服务器端集成Script标签返回至客户端,通过JavaScript callback的形式实现跨域访问。
目录
JSONP基本原理
同源策略限制
浏览器的同源策略(Same-Origin Policy)是Web安全的基础,它限制了一个源的文档或脚本如何与另一个源的资源进行交互。同源的定义是:协议、域名、端口都相同。
JSONP工作原理
JSONP通过<script>标签绕过同源策略限制,因为<script>标签的src属性不受同源策略限制。
核心机制
- 动态创建script标签:前端动态创建
<script>元素 - 指定回调函数:通过URL参数传递回调函数名
- 服务器包装响应:服务器将JSON数据包装在回调函数中
- 执行回调:浏览器接收到响应后自动执行回调函数
JSONP源码解析
前端实现源码
// 基础JSONP实现
function jsonp(url, callback, callbackName) {
// 生成唯一的回调函数名
const uniqueCallbackName = callbackName || 'jsonp_callback_' + Math.round(100000 * Math.random());
// 创建script标签
const script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + uniqueCallbackName;
// 定义全局回调函数
window[uniqueCallbackName] = function(data) {
// 清理工作
cleanup();
// 执行用户回调
callback(data);
};
// 错误处理
script.onerror = function() {
cleanup();
callback(new Error('JSONP request failed'));
};
// 清理函数
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[uniqueCallbackName];
}
// 添加到DOM
document.head.appendChild(script);
// 设置超时
const timeoutId = setTimeout(() => {
cleanup();
callback(new Error('JSONP request timeout'));
}, 10000);
// 覆盖清理函数以清除超时
const originalCleanup = cleanup;
cleanup = function() {
clearTimeout(timeoutId);
originalCleanup();
};
}
// 使用示例
jsonp('https://api.example.com/data', function(data) {
console.log('Received data:', data);
});
高级JSONP封装
class JSONP {
constructor(options = {}) {
this.timeout = options.timeout || 10000;
this.prefix = options.prefix || '__jp';
this.counter = 0;
}
request(url, options = {}) {
return new Promise((resolve, reject) => {
const callbackName = `${this.prefix}${++this.counter}`;
const script = document.createElement('script');
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('JSONP request timeout'));
}, this.timeout);
// 设置全局回调
window[callbackName] = function(data) {
cleanup();
resolve(data);
};
// 错误处理
script.onerror = function() {
cleanup();
reject(new Error('JSONP request failed'));
};
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callbackName];
clearTimeout(timeoutId);
}
// 构建URL
const separator = url.indexOf('?') >= 0 ? '&' : '?';
script.src = `${url}${separator}callback=${callbackName}`;
// 添加额外参数
if (options.params) {
Object.keys(options.params).forEach(key => {
script.src += `&${key}=${encodeURIComponent(options.params[key])}`;
});
}
document.head.appendChild(script);
});
}
}
// 使用示例
const jsonpClient = new JSONP({ timeout: 5000 });
jsonpClient.request('https://api.example.com/data', {
params: { userId: 123 }
}).then(data => {
console.log('Data received:', data);
}).catch(error => {
console.error('Error:', error);
});
后端实现源码
Node.js/Express实现
const express = require('express');
const app = express();
// JSONP中间件
function jsonpMiddleware(req, res, next) {
const originalJson = res.json;
res.json = function(data) {
const callback = req.query.callback;
if (callback) {
// 验证回调函数名(防止XSS)
if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(callback)) {
return res.status(400).send('Invalid callback name');
}
// 设置正确的Content-Type
res.set('Content-Type', 'application/javascript');
// 包装JSON数据
const jsonpResponse = `${callback}(${JSON.stringify(data)})`;
res.send(jsonpResponse);
} else {
originalJson.call(this, data);
}
};
next();
}
// 使用中间件
app.use(jsonpMiddleware);
// JSONP接口示例
app.get('/api/data', (req, res) => {
const data = {
message: 'Hello from JSONP API',
timestamp: new Date().toISOString(),
data: [1, 2, 3, 4, 5]
};
res.json(data); // 自动处理JSONP
});
app.listen(3000, () => {
console.log('JSONP server running on port 3000');
});
Java/Spring Boot实现
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class JsonpController {
@GetMapping("/api/data")
public Object getData(
@RequestParam(required = false) String callback,
@RequestParam(required = false, defaultValue = "json") String format) {
Map<String, Object> data = new HashMap<>();
data.put("message", "Hello from JSONP API");
data.put("timestamp", new Date().toISOString());
data.put("data", Arrays.asList(1, 2, 3, 4, 5));
// 如果是JSONP请求
if (callback != null && !callback.isEmpty()) {
// 验证回调函数名
if (!callback.matches("^[a-zA-Z_$][0-9a-zA-Z_$]*$")) {
throw new IllegalArgumentException("Invalid callback name");
}
// 创建JSONP响应
String jsonpResponse = callback + "(" + new ObjectMapper().writeValueAsString(data) + ")";
return new ResponseEntity<>(jsonpResponse, HttpStatus.OK);
}
// 普通JSON响应
return data;
}
}
PHP实现
<?php
header('Content-Type: application/javascript');
// 获取回调函数名
$callback = isset($_GET['callback']) ? $_GET['callback'] : null;
// 准备数据
$data = array(
'message' => 'Hello from JSONP API',
'timestamp' => date('c'),
'data' => array(1, 2, 3, 4, 5)
);
// 验证回调函数名
if ($callback && preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $callback)) {
// 返回JSONP格式
echo $callback . '(' . json_encode($data) . ');';
} else {
// 返回普通JSON
header('Content-Type: application/json');
echo json_encode($data);
}
?>
前端实现方案
1. 原生JavaScript实现
// 简单版本
function simpleJsonp(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Date.now();
window[callbackName] = function(data) {
callback(data);
document.head.removeChild(script);
delete window[callbackName];
};
script.src = url + '?callback=' + callbackName;
document.head.appendChild(script);
}
// 增强版本
function advancedJsonp(url, options = {}) {
return new Promise((resolve, reject) => {
const {
timeout = 10000,
params = {},
callbackName = 'callback'
} = options;
const script = document.createElement('script');
const uniqueCallback = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 构建URL参数
const queryParams = new URLSearchParams({
[callbackName]: uniqueCallback,
...params
});
const fullUrl = `${url}${url.includes('?') ? '&' : '?'}${queryParams}`;
// 超时处理
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('JSONP timeout'));
}, timeout);
// 全局回调函数
window[uniqueCallback] = function(data) {
cleanup();
resolve(data);
};
// 错误处理
script.onerror = () => {
cleanup();
reject(new Error('JSONP request failed'));
};
function cleanup() {
clearTimeout(timeoutId);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[uniqueCallback];
}
script.src = fullUrl;
document.head.appendChild(script);
});
}
2. jQuery实现
// jQuery的$.ajax支持JSONP
$.ajax({
url: 'https://api.example.com/data',
dataType: 'jsonp',
jsonp: 'callback', // 回调函数参数名
jsonpCallback: 'myCallback', // 指定回调函数名
success: function(data) {
console.log('Success:', data);
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
// 简写形式
$.getJSON('https://api.example.com/data?callback=?', function(data) {
console.log('Data:', data);
});
3. Vue.js实现
// Vue组件中使用JSONP
export default {
methods: {
fetchDataWithJsonp() {
this.jsonp('https://api.example.com/data', {
params: { userId: 123 }
}).then(data => {
this.data = data;
}).catch(error => {
console.error('JSONP Error:', error);
});
},
jsonp(url, options = {}) {
return new Promise((resolve, reject) => {
const callbackName = `jsonp_callback_${Date.now()}`;
const script = document.createElement('script');
window[callbackName] = function(data) {
resolve(data);
cleanup();
};
const params = new URLSearchParams({
callback: callbackName,
...options.params
});
script.src = `${url}?${params}`;
script.onerror = () => {
reject(new Error('JSONP failed'));
cleanup();
};
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callbackName];
}
document.head.appendChild(script);
setTimeout(() => {
cleanup();
reject(new Error('JSONP timeout'));
}, 10000);
});
}
}
}
4. React Hook实现
import { useState, useEffect } from 'react';
function useJsonp(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
setLoading(true);
setError(null);
const callbackName = `jsonp_callback_${Date.now()}`;
const script = document.createElement('script');
window[callbackName] = function(data) {
setData(data);
setLoading(false);
cleanup();
};
script.onerror = () => {
setError(new Error('JSONP request failed'));
setLoading(false);
cleanup();
};
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callbackName];
}
const params = new URLSearchParams({
callback: callbackName,
...options.params
});
script.src = `${url}?${params}`;
document.head.appendChild(script);
const timeoutId = setTimeout(() => {
cleanup();
setError(new Error('JSONP timeout'));
setLoading(false);
}, options.timeout || 10000);
return () => {
clearTimeout(timeoutId);
cleanup();
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// 使用示例
function MyComponent() {
const { data, loading, error } = useJsonp(
'https://api.example.com/data',
{ params: { userId: 123 } }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Data: {JSON.stringify(data)}</div>;
}
后端实现方案
1. Node.js/Express
const express = require('express');
const app = express();
// 方法1:手动处理JSONP
app.get('/api/data1', (req, res) => {
const callback = req.query.callback;
const data = { message: 'Hello World', timestamp: new Date() };
if (callback) {
// 验证回调函数名
if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(callback)) {
return res.status(400).json({ error: 'Invalid callback name' });
}
res.set('Content-Type', 'application/javascript');
res.send(`${callback}(${JSON.stringify(data)})`);
} else {
res.json(data);
}
});
// 方法2:使用中间件
function enableJsonp(req, res, next) {
const originalJson = res.json;
res.json = function(data) {
const callback = req.query.callback;
if (callback && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(callback)) {
res.set('Content-Type', 'application/javascript');
return res.send(`${callback}(${JSON.stringify(data)})`);
}
return originalJson.call(this, data);
};
next();
}
app.use(enableJsonp);
app.get('/api/data2', (req, res) => {
res.json({ message: 'Hello from middleware', data: [1, 2, 3] });
});
// 方法3:使用express-jsonp包
const jsonp = require('express-jsonp');
app.use(jsonp());
app.get('/api/data3', (req, res) => {
res.jsonp({ message: 'Hello from jsonp package' });
});
2. Java/Spring Boot
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/api")
public class JsonpController {
private final ObjectMapper objectMapper = new ObjectMapper();
// 方法1:基础实现
@GetMapping(value = "/data1", produces = "application/javascript")
public String getData1(@RequestParam(required = false) String callback)
throws JsonProcessingException {
Map<String, Object> data = new HashMap<>();
data.put("message", "Hello from Spring Boot");
data.put("timestamp", new Date());
data.put("items", Arrays.asList("item1", "item2", "item3"));
String json = objectMapper.writeValueAsString(data);
if (callback != null && isValidCallback(callback)) {
return callback + "(" + json + ")";
}
return json;
}
// 方法2:使用ResponseEntity
@GetMapping("/data2")
public ResponseEntity<?> getData2(@RequestParam(required = false) String callback)
throws JsonProcessingException {
Map<String, Object> data = new HashMap<>();
data.put("status", "success");
data.put("data", Arrays.asList(1, 2, 3, 4, 5));
if (callback != null && isValidCallback(callback)) {
String jsonpResponse = callback + "(" + objectMapper.writeValueAsString(data) + ")";
return ResponseEntity.ok()
.contentType(MediaType.valueOf("application/javascript"))
.body(jsonpResponse);
}
return ResponseEntity.ok(data);
}
// 方法3:自定义注解
@Jsonp
@GetMapping("/data3")
public Map<String, Object> getData3() {
Map<String, Object> data = new HashMap<>();
data.put("custom", "annotation");
data.put("value", "Hello");
return data;
}
private boolean isValidCallback(String callback) {
return callback.matches("^[a-zA-Z_$][0-9a-zA-Z_$]*$");
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Jsonp {
}
// 注解处理器
@Component
public class JsonpInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Jsonp jsonpAnnotation = handlerMethod.getMethodAnnotation(Jsonp.class);
if (jsonpAnnotation != null) {
String callback = request.getParameter("callback");
if (callback != null && callback.matches("^[a-zA-Z_$][0-9a-zA-Z_$]*$")) {
response.setContentType("application/javascript");
// 这里需要自定义消息转换器来处理JSONP
}
}
}
}
}
3. PHP实现
<?php
class JsonpHandler {
public static function sendJsonp($data, $callback = null) {
// 设置响应头
header('Access-Control-Allow-Origin: *');
if ($callback && self::isValidCallback($callback)) {
// JSONP响应
header('Content-Type: application/javascript');
echo $callback . '(' . json_encode($data) . ');';
} else {
// 普通JSON响应
header('Content-Type: application/json');
echo json_encode($data);
}
}
private static function isValidCallback($callback) {
return preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $callback);
}
}
// 使用示例
$data = array(
'status' => 'success',
'message' => 'Hello from PHP JSONP',
'data' => array(1, 2, 3, 4, 5)
);
JsonpHandler::sendJsonp($data, $_GET['callback'] ?? null);
?>
<!-- Laravel框架中的实现 -->
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ApiController extends Controller {
public function getData(Request $request) {
$data = [
'message' => 'Hello from Laravel',
'timestamp' => now()->toISOString(),
'items' => [1, 2, 3, 4, 5]
];
$callback = $request->get('callback');
if ($callback && preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $callback)) {
return response($callback . '(' . json_encode($data) . ');')
->header('Content-Type', 'application/javascript');
}
return response()->json($data);
}
}
?>
4. Python/Flask实现
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
def send_jsonp(data, callback=None):
"""发送JSONP响应"""
if callback and is_valid_callback(callback):
# JSONP响应
response_data = f"{callback}({json.dumps(data)})"
return app.response_class(
response_data,
mimetype='application/javascript'
)
else:
# 普通JSON响应
return jsonify(data)
def is_valid_callback(callback):
"""验证回调函数名"""
import re
return bool(re.match(r'^[a-zA-Z_$][0-9a-zA-Z_$]*$', callback))
@app.route('/api/data')
def get_data():
data = {
'message': 'Hello from Flask',
'timestamp': '2023-01-01T00:00:00Z',
'items': [1, 2, 3, 4, 5]
}
callback = request.args.get('callback')
return send_jsonp(data, callback)
# 使用装饰器的方式
from functools import wraps
def jsonp_support(f):
@wraps(f)
def decorated_function(*args, **kwargs):
callback = request.args.get('callback')
result = f(*args, **kwargs)
if callback and is_valid_callback(callback):
data = json.dumps(result.get_json() if hasattr(result, 'get_json') else result)
return app.response_class(
f"{callback}({data})",
mimetype='application/javascript'
)
return result
return decorated_function
@app.route('/api/data2')
@jsonp_support
def get_data2():
return jsonify({
'message': 'Hello from decorator',
'data': [1, 2, 3]
})
安全性考虑
1. 回调函数名验证
// 前端验证
function validateCallbackName(name) {
return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
}
// 后端验证(Node.js)
function isValidCallback(callback) {
return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(callback);
}
2. 内容安全策略(CSP)
// 设置CSP头
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"script-src 'self' https://trusted-domain.com");
next();
});
3. 输入验证和过滤
// 过滤危险字符
function sanitizeInput(input) {
return input.replace(/[<>]/g, '');
}
// 限制回调函数长度
function validateCallbackLength(callback) {
return callback && callback.length <= 50;
}
4. HTTPS和Referer检查
// 检查Referer
app.use((req, res, next) => {
const referer = req.get('Referer');
const allowedDomains = ['https://mydomain.com', 'https://subdomain.mydomain.com'];
if (referer && !allowedDomains.some(domain => referer.startsWith(domain))) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
});
现代替代方案
1. CORS (跨域资源共享)
// 前端使用fetch
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => response.json())
.then(data => console.log(data));
// Node.js后端CORS设置
const cors = require('cors');
app.use(cors({
origin: ['http://localhost:3000', 'https://myapp.com'],
credentials: true
}));
2. 代理服务器
// 前端请求同域代理
fetch('/api/proxy?url=https://external-api.com/data')
.then(response => response.json())
.then(data => console.log(data));
// Node.js代理服务器
const httpProxy = require('http-proxy-middleware');
app.use('/api/proxy', httpProxy({
target: 'https://external-api.com',
changeOrigin: true,
pathRewrite: {
'^/api/proxy': ''
}
}));
3. postMessage跨域通信
// 父窗口
const iframe = document.createElement('iframe');
iframe.src = 'https://child-domain.com/page.html';
document.body.appendChild(iframe);
window.addEventListener('message', (event) => {
if (event.origin !== 'https://child-domain.com') return;
console.log('Received data:', event.data);
});
// 子窗口
window.parent.postMessage({ type: 'DATA', payload: data }, 'https://parent-domain.com');
总结
JSONP是一种巧妙的跨域解决方案,通过利用<script>标签不受同源策略限制的特性来实现跨域数据获取。虽然现代Web开发中CORS已经成为主流选择,但理解JSONP的工作原理仍然很有价值:
-
优点:
- 兼容性好,支持老旧浏览器
- 实现简单,不需要服务器特殊配置
- 支持GET请求跨域
-
缺点:
- 仅支持GET请求
- 存在安全风险(XSS攻击)
- 错误处理困难
- 逐渐被CORS取代
-
使用场景:
- 需要兼容老旧浏览器的项目
- 第三方API只提供JSONP接口
- 简单的跨域数据获取需求
在现代Web开发中,建议优先使用CORS、代理服务器或postMessage等更安全、功能更完善的跨域解决方案。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)