Tornado: Manipulando Requisições AJAX, Upload de Arquivos e Requisições Cross-Origin

Este guia explora funcionalidades avançadas no framework web Tornado, focando em como lidar com requisições AJAX, realizar uploads de arquivos e gerenciar requisições cross-origin (entre diferentes domínios).

AJAX e Requisições Web

AJAX (Asynchronous JavaScript and XML) permite atualizações dinâmicas de páginas web sem a necessidade de recarregar a página inteira. O Tornado pode servir como backend para essas requisições.

1. Simulação de AJAX com Iframe

Uma técnica antiga para simular requisições assíncronas envolvia o uso de iframes ocultos. Embora menos comum hoje em dia, é útil para entender a evolução das requisições web.

Exemplo de Servidor (app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("index.html")

   def post(self):
       self.write("Página POST de validação CSRF")

settings = {
   "template_path": "views",
   "static_path": "static",
   "xsrf_cookies": True # Habilita a validação CSRF
}

app = tornado.web.Application([
   (r"/index", IndexHandler)
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (index.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Simulação AJAX com Iframe</title>
</head>
<body>
 <div>
   <p>Insira o URL para carregar:<span id="currentTime"></span></p>
   <p>
       <input type="text" id="url">
       <input type="button" value="Recarregar" onclick="loadPage()">
   </p>
 </div>
 <div>
     <h3>Conteúdo carregado aqui:</h3>
     <iframe id="iframe" frameborder="0" style="height:500px;width:500px; border: 1px solid red"></iframe>
 </div>
 <script>
     window.onload = function() {
         var mydate = new Date();
         document.getElementById("currentTime").innerText = mydate.getTime();
     }
     function loadPage() {
         var url = document.getElementById("url").value;
         document.getElementById("iframe").src = url;
     }
 </script>
</body>
</html>
   

Ao executar app.py e acessar 127.0.0.1:8092/index, o navegador exibirá o template index.html. A página carrega o timestamp atual e permite ao usuário inserir um URL para ser carregado dentro da <iframe>.

2. AJAX com XMLHttpRequest Nativo

O objeto XMLHttpRequest é a base para requisições AJAX no navegador.

Exemplo de Servidor (app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("index.html")

   def post(self):
       self.write("Página de resposta AJAX")

settings = {
   "template_path": "views",
   "static_path": "static",
   # "xsrf_cookies": True # Desabilitado para este exemplo
}

app = tornado.web.Application([
   (r"/index", IndexHandler)
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (index.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>AJAX com XMLHttpRequest</title>
</head>
<body>
<h1>XMLHttpRequest - Requisições AJAX</h1>
<input type="button" onclick="sendXhrRequest('GET');" value="GET Request" />
<input type="button" onclick="sendXhrRequest('POST');" value="POST Request" />

<script type="text/javascript">
   function getXHR() {
       var xhr = null;
       if (window.XMLHttpRequest) {
           xhr = new XMLHttpRequest();
       } else {
           xhr = new ActiveXObject("Microsoft.XMLHTTP");
       }
       return xhr;
   }

   function sendXhrRequest(method) {
       var xhr = getXHR();
       xhr.onreadystatechange = function() {
           if (xhr.readyState == 4) {
               if (xhr.status == 200) {
                   console.log("Resposta:", xhr.responseText);
               } else {
                   console.error("Erro na requisição:", xhr.status);
               }
           }
       };

       xhr.open(method, "/index", true);
       if (method === 'POST') {
           xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
           xhr.send('param1=valor1&param2=valor2');
       } else {
           xhr.send();
       }
   }
</script>
</body>
</html>
   

Este exemplo demonstra como usar XMLHttpRequest para enviar requisições GET e POST para o endpoint /index do servidor Tornado.

3. AJAX com jQuery

A biblioteca jQuery simplifica a implementação de requisições AJAX.

Funções jQuery para AJAX:

  • $.get(url, [data], [callback], [dataType])
  • $.post(url, [data], [callback], [dataType])
  • $.getJSON(url, [data], [callback])
  • $.getScript(url, [callback])
  • $.ajax(settings): Método mais flexível e completo.

Parâmetros importantes do $.ajax():

  • url: Endereço da requisição.
  • type (ou method): Método HTTP (GET, POST).
  • data: Dados a serem enviados.
  • dataType: Tipo de dado esperado da resposta (xml, json, text, html, script, jsonp).
  • success: Função de callback para requisições bem-sucedidas.
  • error: Função de callback para requisições com falha.
  • contentType: Tipo de conteúdo enviado.
  • headers: Cabeçalhos HTTP customizados.

Exemplo de Servidor (app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       param_v1 = self.get_argument("v1", None)
       print(f"GET request received with v1: {param_v1}")
       self.write(f"GET request processed. Received: {param_v1}")

   def post(self):
       param_v1 = self.get_argument("v1", None)
       print(f"POST request received with v1: {param_v1}")
       self.write(f"POST request processed. Received: {param_v1}")

settings = {
   "template_path": "views",
   "static_path": "static",
   # "xsrf_cookies": True # Desabilitado para este exemplo
}

app = tornado.web.Application([
   (r"/index", IndexHandler)
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (index.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>AJAX com jQuery</title>
</head>
<body>
<h1>AJAX com jQuery</h1>
<input type="button" onclick="sendJqueryGet();" value="jQuery GET" />
<input type="button" onclick="sendJqueryPost();" value="jQuery POST" />

<script src="{{ static_url('jquery.js') }}"></script>
<script type="text/javascript">
   function sendJqueryGet() {
       $.ajax({
           url: "/index",
           type: 'GET',
           data: {"v1": "Requisição GET com jQuery"},
           dataType: 'text', // Espera texto como resposta
           success: function(data, statusText, jqXHR) {
               console.log("Sucesso (GET):", data);
           },
           error: function() {
               alert("Erro na requisição GET!");
           }
       });
   }

   function sendJqueryPost() {
       $.ajax({
           url: "/index",
           type: 'POST',
           data: {"v1": "Requisição POST com jQuery"},
           dataType: 'text', // Espera texto como resposta
           success: function(data, statusText, jqXHR) {
               console.log("Sucesso (POST):", data);
           },
           error: function() {
               alert("Erro na requisição POST!");
           }
       });
   }
</script>
</body>
</html>
   

Ao acessar 127.0.0.1:8092/index e clicar nos botões, requisições AJAX GET e POST são enviadas usando jQuery para o servidor Tornado.

Upload de Arquivos

O Tornado facilita o tratamento de uploads de arquivos, tanto via formulários HTML tradicionais quanto via AJAX.

1. Upload via Formulário HTML

Um formulário com enctype="multipart/form-data" é a maneira padrão de lidar com uploads.

Exemplo de Servidor (app.py):


import tornado.web
import tornado.ioloop
import os

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("index.html")
   def post(self):
       self.write("Página de resposta AJAX")

img_list = [] # Lista para armazenar nomes de imagens
class FileUploadHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("upload_file.html", img=img_list)

   def post(self):
       user = self.get_argument("user")
       favors = self.get_arguments("favor") # Para checkboxes
       
       files = self.request.files.get("fafa") # 'fafa' é o name do input type="file"
       if not files:
           self.write("Nenhum arquivo enviado.")
           return

       for file_info in files:
           filename = file_info["filename"]
           body = file_info["body"]
           save_path = os.path.join("static", "img", filename)
           
           # Cria o diretório se não existir
           os.makedirs(os.path.dirname(save_path), exist_ok=True)
           
           with open(save_path, "wb") as f:
               f.write(body)
           img_list.append(filename)
       
       self.redirect("/uploadFile") # Redireciona para mostrar a lista de imagens

settings = {
   "template_path": "views",
   "static_path": "static",
   # "xsrf_cookies": True
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
   (r"/uploadFile", FileUploadHandler),
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (upload_file.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Upload de Arquivo (Form)</title>
</head>
<body>
   <form action="/uploadFile" method="post" enctype="multipart/form-data">
       <input type="text" name="user" placeholder="Nome de usuário"><br>
       <h3>Interesses:</h3>
       <input type="checkbox" name="favor" value="1"> Futebol
       <input type="checkbox" name="favor" value="2"> Basquete
       <input type="checkbox" name="favor" value="3"> Vôlei<br>

       <p>
           <input type="file" name="fafa"> <!-- name="fafa" é crucial -->
       </p>
       <p>
           <input type="submit" value="Enviar">
       </p>
   </form>

   <ul>
       {% for item in img %}
           <li><img src="static/img/{{ item }}" alt="{{ item }}" style="max-width: 200px;"></li>
       {% end %}
   </ul>
</body>
</html>
   

Acessando 127.0.0.1:8092/uploadFile, o usuário pode enviar um arquivo que será salvo no diretório static/img.

2. Upload via AJAX

Realizar uploads de forma assíncrona melhora a experiência do usuário.

Exemplo de Servidor (app.py - expandido):


import tornado.web
import tornado.ioloop
import os

# ... (IndexHandler permanece o mesmo) ...

img_list = []
class FileUploadHandler(tornado.web.RequestHandler):
   # ... (método GET igual ao anterior) ...
   def get(self):
       self.render("upload_file.html", img=img_list)

   def post(self):
       # ... (lógica de processamento do formulário igual ao anterior) ...
       user = self.get_argument("user")
       favors = self.get_arguments("favor")
       files = self.request.files.get("fafa")
       if not files:
           self.write("Nenhum arquivo enviado.")
           return
       for file_info in files:
           filename = file_info["filename"]
           body = file_info["body"]
           save_path = os.path.join("static", "img", filename)
           os.makedirs(os.path.dirname(save_path), exist_ok=True)
           with open(save_path, "wb") as f:
               f.write(body)
           img_list.append(filename)
       self.redirect("/uploadFile")


class FileUploadAjaxHandler(tornado.web.RequestHandler):
   def get(self):
       # Renderiza um template diferente para o upload via AJAX
       self.render("upload_file_ajax.html", img=img_list)

   def post(self):
       # Lógica similar ao POST do FileUploadHandler, mas não faz redirect
       # Poderia retornar um JSON com o status ou o nome do arquivo
       user = self.get_argument("user")
       favors = self.get_arguments("favor")
       files = self.request.files.get("fafa")
       if not files:
           self.write('{"status": "error", "message": "Nenhum arquivo enviado"}')
           return
       
       for file_info in files:
           filename = file_info["filename"]
           body = file_info["body"]
           save_path = os.path.join("static", "img", filename)
           os.makedirs(os.path.dirname(save_path), exist_ok=True)
           with open(save_path, "wb") as f:
               f.write(body)
           img_list.append(filename)
       
       self.write('{"status": "success", "filename": "%s"}' % filename)


settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
   (r"/uploadFile", FileUploadHandler),
   (r"/uploadFileAjax", FileUploadAjaxHandler),
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (upload_file_ajax.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Upload de Arquivo (AJAX)</title>
</head>
<body>
   <p>
       <input type="file" id="fileInput" name="fafa">
       <button onclick="uploadFileViaAjax()">Upload AJAX</button>
   </p>
   <div id="statusMessage"></div>

   <script>
       function uploadFileViaAjax() {
           var fileInput = document.getElementById("fileInput");
           var file = fileInput.files[0];
           var formData = new FormData();
           
           formData.append("user", "ajax_user"); // Exemplo de outros campos
           formData.append("favor", "1");
           formData.append("fafa", file); // 'fafa' deve corresponder ao nome esperado no servidor

           var xhr = new XMLHttpRequest();
           xhr.open("POST", "/uploadFileAjax", true);

           xhr.onload = function() {
               if (xhr.status == 200) {
                   var response = JSON.parse(xhr.responseText);
                   document.getElementById("statusMessage").innerText = "Upload: " + response.filename + " - Status: " + response.status;
                   console.log("Resposta do servidor:", response);
               } else {
                   document.getElementById("statusMessage").innerText = "Erro no upload.";
                   console.error("Erro:", xhr.statusText);
               }
           };

           xhr.onerror = function() {
                document.getElementById("statusMessage").innerText = "Erro de rede.";
           };

           xhr.send(formData);
       }
   </script>
</body>
</html>
   

Acessando 127.0.0.1:8092/uploadFileAjax, o usuário pode selecionar um arquivo e enviá-lo via AJAX.

3. Upload com jQuery AJAX

O jQuery simplifica ainda mais o processo de upload com AJAX.

Exemplo de Cliente (upload_file_ajax.html - usando jQuery):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Upload de Arquivo (jQuery AJAX)</title>
</head>
<body>
   <p>
       <input type="file" id="fileInput" name="fafa">
       <button onclick="uploadFileViaJqueryAjax()">Upload jQuery AJAX</button>
   </p>
   <div id="statusMessage"></div>

   <script src="{{ static_url('jquery.js') }}"></script>
   <script>
       function uploadFileViaJqueryAjax() {
           var file = $("#fileInput")[0].files[0];
           var formData = new FormData();
           
           formData.append("user", "jquery_ajax_user");
           formData.append("favor", "3");
           formData.append("fafa", file);

           $.ajax({
               url: '/uploadFileAjax',
               type: 'POST',
               data: formData,
               processData: false,  // Necessário para FormData
               contentType: false,  // Necessário para FormData
               success: function(response) {
                   $("#statusMessage").text("Upload: " + response.filename + " - Status: " + response.status);
                   console.log("Resposta do servidor (jQuery):", response);
               },
               error: function(jqXHR, textStatus, errorThrown) {
                    $("#statusMessage").text("Erro no upload via jQuery.");
                   console.error("Erro (jQuery):", textStatus, errorThrown);
               }
           });
       }
   </script>
</body>
</html>
   

4. Upload via Iframe (Método Legado)

Similar à simulação de AJAX, um iframe pode ser usado para receber a resposta do upload de forma não intrusiva.

Exemplo de Servidor (app.py - adicionando FileUploadIframeHandler):


# ... (imports e handlers anteriores) ...

class FileUploadIframeHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("upload_file_iframe.html", img=img_list)

   def post(self):
       # Processa o upload, similar aos outros handlers
       files = self.request.files.get("fafa")
       if files:
           for file_info in files:
               filename = file_info["filename"]
               body = file_info["body"]
               save_path = os.path.join("static", "img", filename)
               os.makedirs(os.path.dirname(save_path), exist_ok=True)
               with open(save_path, "wb") as f:
                   f.write(body)
               img_list.append(filename)
       
       # Em vez de redirecionar, pode retornar uma resposta simples para o iframe
       # O template pode então ler o conteúdo do iframe
       self.write(f"Upload concluído para: {filename}") 
       # Ou um redirect para um template que apenas exibe a mensagem
       # self.redirect("/uploadFileIFrame") # Se o template for o mesmo

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
   (r"/uploadFile", FileUploadHandler),
   (r"/uploadFileAjax", FileUploadAjaxHandler),
   (r"/uploadFileIFrame", FileUploadIframeHandler), # Novo handler
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Exemplo de Cliente (upload_file_iframe.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Upload de Arquivo (Iframe)</title>
   <style>
       .hidden { display: none; }
   </style>
</head>
<body>
<form id="uploadForm" name="uploadForm" action="/uploadFileIFrame" method="POST" enctype="multipart/form-data">
   <div id="main">
       <input name="fafa" id="myFile" type="file" />
       <button type="button" onclick="initiateUpload()">Upload</button>
       <iframe id='uploadIframe' name='uploadIframe' src="" class="hidden"></iframe>
   </div>
</form>

<script src="{{ static_url('jquery.js') }}"></script>
<script>
   function initiateUpload() {
       document.getElementById('uploadIframe').onload = handleIframeLoad;
       document.getElementById('uploadForm').target = 'uploadIframe';
       document.getElementById('uploadForm').submit();
   }

   function handleIframeLoad() {
       var iframeContent = $("#uploadIframe").contents().find("body").text();
       console.log("Conteúdo do Iframe:", iframeContent);
       // Processar iframeContent conforme necessário
       // Ex: alert("Upload concluído: " + iframeContent);
   }
</script>
</body>
</html>
   

Acessando 127.0.0.1:8092/uploadFileIFrame, o upload é realizado e a resposta é recebida pelo iframe oculto.

Requisições AJAX Cross-Origin (Cross-Domain)

O navegador impõe a política de mesma origem (Same-Origin Policy), que restringe requisições AJAX para domínios diferentes. Para contornar isso, existem técnicas como JSONP e CORS.

Entendendo a Política de Mesma Origem

Requisições AJAX padrão só funcionam se o script que as executa e o servidor de destino compartilharem o mesmo protocolo, domínio e porta.

Exemplo de Configuração de Servidores Separados:

  • Servidor A (day0403/app.py) em localhost:8092.
  • Servidor B (day0403_t1/app.py) em localhost:8093.

Servidor A (day0403/app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       self.write("Resposta do Servidor A (8092)")
   def post(self):
       self.write("POST do Servidor A (8092)")

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Servidor B (day0403_t1/app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       self.render("index.html") # Renderiza o HTML com o botão
   def post(self):
       self.write("POST do Servidor B (8093)")

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
], **settings)

if __name__ == "__main__":
   app.listen(8093)
   tornado.ioloop.IOLoop.instance().start()
   

Cliente em Servidor B (index.html no Servidor B):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>Requisição Cross-Origin</title>
</head>
<body>
<h1>Teste de Requisição Cross-Origin</h1>
<input type="button" onclick="makeCrossDomainRequest();" value="Tentar Requisição Cross-Origin" />

<script src="{{ static_url('jquery.js') }}"></script>
<script>
   function makeCrossDomainRequest() {
       $.ajax({
           url: "http://localhost:8092/index", // URL do Servidor A
           type: "POST",
           data: {"k1": "valor_do_servidor_B"},
           success: function(response) {
               console.log("Sucesso (Cross-Origin):", response);
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro (Cross-Origin):", textStatus, errorThrown);
               // Geralmente, um erro aqui indica que a política de mesma origem bloqueou a requisição
           }
       });
   }
</script>
</body>
</html>
   

Ao executar ambos os servidores e acessar o Servidor B, a tentativa de requisição POST para o Servidor A falhará devido à política de mesma origem, resultando em um erro no console do navegador.

1. Contornando com JSONP

JSONP (JSON with Padding) é uma técnica que explora o fato de que tags <script> podem carregar recursos de domínios diferentes. O servidor retorna um script que chama uma função global definida no cliente.

Servidor A Modificado (day0403/app.py):


import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
   def get(self):
       # Espera um parâmetro 'callback' da requisição
       callback_func = self.get_argument("callback", "callback") 
       # Retorna uma chamada de função com os dados
       data = [11, 22, 33]
       self.write(f"{callback_func}({tornado.escape.json_encode(data)});")

   def post(self):
       self.write("POST do Servidor A (8092)")

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Cliente Modificado (index.html no Servidor B):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>JSONP Cross-Origin</title>
</head>
<body>
<h1>JSONP Cross-Origin</h1>
<input type="button" onclick="makeJsonpRequest();" value="Requisição JSONP" />

<script src="{{ static_url('jquery.js') }}"></script>
<script>
   // Função global que será chamada pelo servidor
   function handleJsonResponse(data) {
       console.log("Dados recebidos via JSONP:", data);
   }

   function makeJsonpRequest() {
       $.ajax({
           url: "http://localhost:8092/index", // URL do Servidor A
           dataType: "jsonp", // Indica ao jQuery para usar JSONP
           jsonp: "callback", // Nome do parâmetro de callback esperado pelo servidor
           jsonpCallback: "handleJsonResponse", // Nome da função global a ser usada
           success: function(response) {
               // O callback já foi executado, esta função success pode não ser chamada ou pode ter comportamento diferente dependendo da versão do jQuery
               console.log("AJAX success (JSONP) - pode não ser chamado se o callback já processou");
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro na requisição JSONP:", textStatus, errorThrown);
           }
       });
   }
</script>
</body>
</html>
   

Ao clicar no botão, o jQuery cria dinamicamente uma tag <script> apontando para http://localhost:8092/index?callback=handleJsonResponse. O servidor A responde com handleJsonResponse([11, 22, 33]);, executando a função no cliente.

Exemplo Adicional (Requisição a Serviço Externo): O código mostra como fazer uma requisição JSONP para www.jxntv.cn, demonstrando o uso em cenários reais.

2. Contornando com CORS (Cross-Origin Resource Sharing)

CORS é um padrão W3C que permite que servidores especifiquem quais origens (domínios) têm permissão para acessar seus recursos. Isso é feito através de cabeçalhos HTTP na resposta do servidor.

Tipos de Requisições CORS:

  • Requisições Simples: GET, HEAD, POST (com Content-Type limitado).
  • Requisições Não Simples (Complexas): Métodos como PUT, DELETE, ou POST com Content-Type não padrão. Estas disparam uma "pré-verificação" (preflight request) usando o método OPTIONS.

Configuração do Servidor A (day0403/app.py - CORS):


import tornado.web
import tornado.ioloop
import tornado.escape

class IndexHandler(tornado.web.RequestHandler):
   # ... (handler para GET/POST, possivelmente JSONP) ...
   def get(self):
       callback_func = self.get_argument("callback", "callback") 
       data = [11, 22, 33]
       self.write(f"{callback_func}({tornado.escape.json_encode(data)});")
   def post(self):
       self.write("POST do Servidor A (8092)")

class CorsHandler(tornado.web.RequestHandler):
   def set_common_cors_headers(self):
       # Define o cabeçalho que permite requisições da origem do cliente (Servidor B)
       # Use "*" para permitir qualquer origem (menos seguro)
       # self.set_header("Access-Control-Allow-Origin", "*") 
       self.set_header("Access-Control-Allow-Origin", "http://localhost:8093") # Permitindo especificamente o Servidor B
       self.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS") # Métodos permitidos
       self.set_header("Access-Control-Allow-Headers", "Content-Type, h1") # Cabeçalhos permitidos (inclui customizados)
       self.set_header("Access-Control-Max-Age", 60) # Tempo de cache da pré-verificação (em segundos)

   def options(self):
       # Resposta para a pré-verificação (preflight request)
       self.set_common_cors_headers()
       # Não envia corpo na resposta OPTIONS, apenas cabeçalhos
       self.set_status(204) # No Content

   def get(self):
       self.set_common_cors_headers() # Necessário mesmo para GET se quiser expor headers customizados
       self.write({"status": "1", "message": "GET via CORS"})

   def post(self):
       self.set_common_cors_headers()
       body_data = self.request.body.decode('utf-8')
       self.write({"status": "1", "message": f"POST via CORS recebido: {body_data}"})

   def put(self):
       self.set_common_cors_headers()
       # Expondo headers customizados para o cliente
       self.set_header("n1", "valor_n1") 
       self.set_header("m1", "valor_m1")
       self.set_header('Access-Control-Expose-Headers', "n1, m1") # Lista headers customizados expostos
       
       body_data = self.request.body.decode('utf-8')
       self.write({"status": "1", "message": f"PUT via CORS recebido: {body_data}"})

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/index", IndexHandler),
   (r"/cors", CorsHandler), # Novo handler para CORS
], **settings)

if __name__ == "__main__":
   app.listen(8092)
   tornado.ioloop.IOLoop.instance().start()
   

Cliente no Servidor B (cors.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>CORS Cross-Origin</title>
</head>
<body>
<h1>AJAX CORS</h1>
<input type="button" onclick="makeSimpleCorsRequest('POST');" value="CORS Simples (POST)" />
<input type="button" onclick="makeComplexCorsRequest('PUT');" value="CORS Complexo (PUT)" />
<input type="button" onclick="makeComplexCorsRequestWithCustomHeader('PUT');" value="CORS Complexo (PUT com Header Custom)" />

<script src="{{ static_url('jquery.js') }}"></script>
<script>
   function makeSimpleCorsRequest(method) {
       $.ajax({
           url: "http://localhost:8092/cors", // Servidor A
           type: method,
           data: {"key": "simple_value"},
           success: function(response) {
               console.log("Sucesso CORS Simples:", response);
               // Para ver os headers na resposta, use a aba Network no DevTools
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro CORS Simples:", textStatus, errorThrown);
           }
       });
   }

   function makeComplexCorsRequest(method) {
       // Requisição PUT é considerada complexa
       $.ajax({
           url: "http://localhost:8092/cors", // Servidor A
           type: method,
           data: {"key": "complex_value"},
           success: function(response, statusText, jqXHR) {
               console.log("Sucesso CORS Complexo:", response);
                // Para ver os headers customizados (n1, m1), precisa usar getAllResponseHeaders()
               console.log("Headers da resposta:", jqXHR.getAllResponseHeaders());
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro CORS Complexo:", textStatus, errorThrown);
           }
       });
   }
   
   function makeComplexCorsRequestWithCustomHeader(method) {
       $.ajax({
           url: "http://localhost:8092/cors", // Servidor A
           type: method,
           headers: {"h1": "custom_header_value"}, // Adiciona um header customizado
           data: {"key": "complex_custom_header_value"},
           success: function(response, statusText, jqXHR) {
               console.log("Sucesso CORS Complexo (Custom Header):", response);
               console.log("Headers da resposta:", jqXHR.getAllResponseHeaders());
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro CORS Complexo (Custom Header):", textStatus, errorThrown);
           }
       });
   }
</script>
</body>
</html>
   

Ao clicar nos botões, o navegador primeiro envia uma requisição OPTIONS para o servidor A. Se a pré-verificação for bem-sucedida (baseada nos cabeçalhos Access-Control-... configurados no servidor A), a requisição real (POST, PUT) é enviada.

Transferência de Cookies em Requisições Cross-Origin

Por padrão, cookies não são enviados em requisições cross-origin para prevenir ataques CSRF. Para habilitar o envio:

  • Cliente (XMLHttpRequest): Defina xhrFields: { withCredentials: true }.
  • Servidor (Tornado): Defina Access-Control-Allow-Credentials: true e Access-Control-Allow-Origin para um domínio específico (não pode ser *).

Servidor A (day0403/app.py - configurando Credenciais):


# ... (outros imports e handlers) ...

class CorsHandler(tornado.web.RequestHandler):
   def set_common_cors_headers(self):
       origin = "http://localhost:8093"
       self.set_header("Access-Control-Allow-Origin", origin)
       self.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
       self.set_header("Access-Control-Allow-Headers", "Content-Type, h1")
       self.set_header("Access-Control-Max-Age", 60)
       # Habilita envio de credenciais (cookies)
       self.set_header("Access-Control-Allow-Credentials", "true") 

   def options(self):
       self.set_common_cors_headers()
       self.set_status(204) 

   def get(self):
       # Define um cookie no cliente
       self.set_cookie("session_id", "xyz789", expires_days=1) 
       self.set_common_cors_headers()
       self.write({"status": "1", "message": "GET via CORS com cookie set"})

   def put(self):
       self.set_common_cors_headers()
       # Tenta ler o cookie enviado pelo cliente
       session_cookie = self.get_cookie("session_id")
       print(f"Cookie 'session_id' recebido: {session_cookie}")
       
       # Expondo headers customizados
       self.set_header('Access-Control-Expose-Headers', "n1, m1")
       self.set_header("n1", "valor_n1")
       self.set_header("m1", "valor_m1")

       if session_cookie == "xyz789":
           self.write({"status": "1", "message": "PUT via CORS com cookie válido"})
       else:
           self.write({"status": "0", "message": "Cookie inválido ou ausente"})

# ... (resto do app.py) ...
   

Servidor B (day0403_t1/app.py - definindo cookie e cliente):


import tornado.web
import tornado.ioloop

class CorsHandler(tornado.web.RequestHandler):
   def get(self):
       # Define um cookie diferente para o servidor B, apenas para teste
       self.set_cookie("b_cookie", "value_b", expires_days=1) 
       self.render("cors.html") # Renderiza o HTML com o botão

   def post(self):
       # Endpoint POST simples, não relacionado ao CORS diretamente aqui
       self.write("Servidor B POST")
       
# ... (IndexHandler e resto do app.py) ...

settings = {
   "template_path": "views",
   "static_path": "static",
}

app = tornado.web.Application([
   (r"/cors", CorsHandler), # Handler que renderiza cors.html
], **settings)

if __name__ == "__main__":
   app.listen(8093) # Porta diferente do Servidor A
   tornado.ioloop.IOLoop.instance().start()
   

Cliente no Servidor B (cors.html):



<html lang="pt">
<head>
   <meta charset="UTF-8">
   <title>CORS com Cookies</title>
</head>
<body>
<h1>CORS com Credenciais</h1>
<input type="button" onclick="requestWithCredentials();" value="Requisição PUT com Credenciais" />

<script src="{{ static_url('jquery.js') }}"></script>
<script>
   function requestWithCredentials() {
       $.ajax({
           url: "http://localhost:8092/cors", // Servidor A
           type: "PUT",
           headers: {"h1": "custom_header_value"},
           data: {"key": "credential_test"},
           xhrFields: {
               withCredentials: true // Habilita envio de cookies
           },
           success: function(response, statusText, jqXHR) {
               console.log("Sucesso CORS com Credenciais:", response);
               console.log("Headers da resposta:", jqXHR.getAllResponseHeaders());
           },
           error: function(jqXHR, textStatus, errorThrown) {
               console.error("Erro CORS com Credenciais:", textStatus, errorThrown);
           }
       });
   }
</script>
</body>
</html>
   

Executando ambos os servidores e acessando o Servidor B, a requisição PUT para o Servidor A enviará o cookie session_id. O servidor A o validará e retornará uma resposta apropriada.

Tags: Python Tornado Web Framework ajax XMLHttpRequest

Publicado em 7-1 01:15