JSONP
Vimos que podemos usar JSON para transportar dados entre servidor e cliente, e que isso pode ser feito com relativa segurança. Mas e para buscar dados em outros domínios? Sei que o Twitter tem uma API poderosa para obter dados históricos de tweets, mas sou limitado pela política de mesma origem. Ou seja, a menos que meu cliente esteja no domínio twitter.com, o uso de um XHR get comum me retornará nada mais do que um erro HTTP.
A forma padrão de contornar isso é usar o Cross Origin Resource Sharing (CORS), que agora é implementado pela maioria dos browsers modernos. No entanto, muitos desenvolvedores acham essa abordagem pesada e um tanto pedante.
O JSONP (documentado pela primeira vez por Bob Ippolito em 2005) é uma alternativa simples e efetiva que usa a capacidade das tags script de obter conteúdo de qualquer servidor.
Ele funciona assim: uma tag script tem um atributo src que pode ser configurado para qualquer resource path, como uma URL, e não precisa retornar um arquivo JavaScript. Assim, posso facilmente criar um fluxo JSON dos dados que alimentam meu twitter para meu cliente.
var scriptTag = document.createElement('SCRIPT');
scriptTag.src = "http://www.twitter.com/status/user_timeline/angustweets.json?count=5";
document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);
Essas são boas notícias, exceto pelo fato de não terem absolutamente nenhum efeito em minha página web, a não ser enchê-la com um monte de JSON indisponível. Para poder usar dados de tag script, precisamos interagir com nosso JavaScript existente. É aí que a parte P (ou “padding” – acolchoado) do JSON se apresenta. Se pudermos fazer o servidor envolver (wrap) sua resposta em uma de nossas próprias funções, podemos fazer com que ele seja útil.
OK, aí vai:
var logIt = function(data) {
//print last tweet text
window.console && console.log(data[0].text);
}
var scriptTag = document.createElement('SCRIPT');
scriptTag.src = "http://www.twitter.com/status/user_timeline/angustweets.json?count=5&callback=logIt";
document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);
/* console will log:
@marijnjh actually I like his paren-free proposal (but replacing global w/ modules seems iffy) JS needs to re-assert simplicity as an asset */
Uau, como fiz isso? Bem, não sem muita ajuda do twitter, que juntamente com muitas outras APIs agora suportam requisições JSONP. Note o parâmetro de solicitação extra: callback=loglt. Isso informa o servidor (twitter) para envolver sua resposta em minha função (loglt).
O JSONP parece bem bacana. Por que toda essa confusão?
OK, finalmente estamos prontos e em condição de checar a discussão JSMentors.com a que me referi no artigo anterior. Peter Van der Zee, Kyle Simpson (conhecido como Getify) e outros estão compreensivelmente preocupados com a segurança do JSONP.
Por quê? Porque sempre que chamamos o JSONP, invocamos qualquer código que o servidor coloque em nossas mãos, sem questionamentos, e sem volta. É como ir de olhos vendados a um restaurante e pedir que coloquem comida na sua boca. Em alguns lugares você pode confiar, em outros, não.
Peter recomenda tirar a função padding da resposta e implementá-la manualmente somente depois que a resposta tiver sido confirmada como JSON puro.
A ideia é basicamente sólida, mas ele acrescenta alguns detalhes de implementação. Ele também lamenta a exigência atual do fornecimento de uma variável global. A proposta do Kyle é similar: ele também defende uma verificação pós-resposta, baseada no mime type da tag script – ele sugere a introdução de um novo mime type específico para o JSONP (por exemplo: “application/json-p”), que dispararia tal validação.
Minha solução JSONP
Concordo com o espírito dos argumentos tanto do Kyle quanto do Peter. Aqui vai um framework JSONP leve que considera algumas de suas preocupações. A função evalJSONP é um callback wrapper que usa uma closure para ligar o custom callback aos dados de resposta. O custom callback pode ser de qualquer alcance e, como no exemplo a seguir, pode ser uma função anônima criada dinamicamente. O wrapper evalJSONP assegura que o callback somente será invocado se a resposta JSON for válida.
var jsonp = {
callbackCounter: 0,
fetch: function(url, callback) {
var fn = 'JSONPCallback_' + this.callbackCounter++;
window[fn] = this.evalJSONP(callback);
url = url.replace('=JSONPCallback', '=' + fn);
var scriptTag = document.createElement('SCRIPT');
scriptTag.src = url;
document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);
},
evalJSONP: function(callback) {
return function(data) {
var validJSON = false;
if (typeof data == "string") {
try {validJSON = JSON.parse(data);} catch (e) {
/*invalid JSON*/}
} else {
validJSON = JSON.parse(JSON.stringify(data));
window.console && console.warn(
'response data was not a JSON string');
}
if (validJSON) {
callback(validJSON);
} else {
throw("JSONP call returned invalid or empty JSON");
}
}
}
}
(Atualização: com a sugestão de Brian Grinstead e Jose Antonio Perez , eu acertei o utilitário para suportar carregamento concorrente de scripts).
Eis aqui alguns exemplos de uso...
//The U.S. President's latest tweet...
var obamaTweets = "http://www.twitter.com/status/user_timeline/BARACKOBAMA.json?count=5&callback=JSONPCallback";
jsonp.fetch(obamaTweets, function(data) {console.log(data[0].text)});
/* console logs:
From the Obama family to yours, have a very happy Thanksgiving. http://OFA.BO/W2KMjJ
*/
//The latest reddit...
var reddits = "http://www.reddit.com/.json?limit=1&jsonp=JSONPCallback";
jsonp.fetch(reddits , function(data) {console.log(data.data.children[0].data.title)});
/* console logs:
You may remember my kitten Swarley wearing a tie. Well, he's all grown up now, but he's still all business. (imgur.com)
*/
Note que sites como o twitter.com na verdade retornam JSON sem aspas, o que faz a tag script carregar um objeto JavaScript. Em tais casos, é o método JSON.stringify que na verdade faz a validação, removendo qualquer atributo não compatível com o JSON. Depois disso, eles com certeza passam no teste JSON.parse. E isso é lamentável, porque mesmo que eu possa limpar o objeto de qualquer dado não JSON, nunca terei certeza se o servidor estava tentando me enviar conteúdo malicioso (sinteticamente, um método horroroso para comparar o objeto original recebido com a versão stringfied e parsed) – o melhor que posso fazer é colocar uma advertência no console.
Esclarecendo, isso é um pouco mais seguro, mas não é seguro. Se o provedor do servidor simplesmente escolher ignorar sua solicitação para envolver (wrap) a resposta dele em sua função, então você estará vulnerável, mas, do contrário, o que apresentei deve fazer do uso do JSONP algo muito tranquilo.
Também listado aqui. Espero que seja útil.
Leitura complementar
- Douglas Crockford: Introducing JSON
- Kyle Simpson: Defining Safer JSON-P
- Matt Harris: Twitter API
- ECMA-262 5th Edition 15.12: The JSON Object
⁂
Texto original disponível em http://javascriptweblog.wordpress.com/2010/11/29/json-and-jsonp/
artigo publicado originalmente no iMasters, por Angus Croll