JSONP技术详解与实现方案

JSONP(JSON with Padding)是一种跨域数据交互协议,它允许在服务器端集成Script标签返回至客户端,通过JavaScript callback的形式实现跨域访问。

目录

  1. JSONP基本原理
  2. JSONP源码解析
  3. 前端实现方案
  4. 后端实现方案
  5. 安全性考虑
  6. 现代替代方案

JSONP基本原理

同源策略限制

浏览器的同源策略(Same-Origin Policy)是Web安全的基础,它限制了一个源的文档或脚本如何与另一个源的资源进行交互。同源的定义是:协议、域名、端口都相同。

请求

被阻止

允许

返回脚本

浏览器

同源服务器

不同源服务器

JSONP请求

JSONP工作原理

JSONP通过<script>标签绕过同源策略限制,因为<script>标签的src属性不受同源策略限制。

后端服务 前端代码 浏览器 后端服务 前端代码 浏览器 创建script标签 请求callback=handleResponse 返回 handleResponse({data: "..."}) 执行回调函数

核心机制

  1. 动态创建script标签:前端动态创建<script>元素
  2. 指定回调函数:通过URL参数传递回调函数名
  3. 服务器包装响应:服务器将JSON数据包装在回调函数中
  4. 执行回调:浏览器接收到响应后自动执行回调函数

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的工作原理仍然很有价值:

  1. 优点

    • 兼容性好,支持老旧浏览器
    • 实现简单,不需要服务器特殊配置
    • 支持GET请求跨域
  2. 缺点

    • 仅支持GET请求
    • 存在安全风险(XSS攻击)
    • 错误处理困难
    • 逐渐被CORS取代
  3. 使用场景

    • 需要兼容老旧浏览器的项目
    • 第三方API只提供JSONP接口
    • 简单的跨域数据获取需求

在现代Web开发中,建议优先使用CORS、代理服务器或postMessage等更安全、功能更完善的跨域解决方案。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐