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¶m2=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(oumethod): 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) emlocalhost:8092. - Servidor B (
day0403_t1/app.py) emlocalhost: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-Typelimitado). - Requisições Não Simples (Complexas): Métodos como PUT, DELETE, ou POST com
Content-Typenã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: trueeAccess-Control-Allow-Originpara 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.