[ Conteúdo ] O que é CORS e porque ele é tão importante para Web
introdução
Hoje venho apresenta-los um dos assuntos mais importantes e que também é muito negligênciado pelo os iniciantes. O intuito aqui é fazer um resumo mastigado do meu entendimento sobre CORS. Não irei me aprofundar muito em coisas que não fazem parte do conceito de CORS, mas se relacionam com ele, como Same-Origin Policy
. Sem mais delongas.
O que é CORS e para quê serve?
Cross-Origin Resource Sharing (CORS) é um HTTP header
que permite diversas coisas que antes não eram possíveis, como ver a posta, a origem do request, scheme, domínios, dentre diversas outras coisas. Tudo isso que citei, é lido pelo o servidor, assim, fazendo uma verificação se as informações são correspondentes ao que foi configurado. Vou deixar mais claro: Quando fazemos um Request usando CORS, antes de enviar o nosso request, internamente o navegador vai enviar um "preflight" que é um header
com diversos paramêtros que vão ser enviados para o servidor ler, e caso esses paramêtros esteja de acordo com o que foi configurado no servidor, o seu request vai ser realizado. Isso é uma técnica de seguraça muito importante que filta bem quais domínions, portas, dentre outras coisas podem ter acesso as informações contidas no Back-end
Outro ponto importante é que CORS permite a requisição de recursos entre navegadores e servidores, que antes, era limitado pelo o sistema Same-origin Policy
que evitava requests a partir de scripts com origins diferentes. Todos os navegadores moderno possui CORS integrado a suas APIs de requisições incluído (XMLHttprequest
, fetch
)
Talvez você esteja na dúvida ainda sobre o porquê do surgimento de CORS. Apesar de eu já ter tido de forma mesmo clara na explicação acima, CORS veio como uma forma de deixar os requests mais seguros e faciltar a requisição de recursos entre navegadores e servidores, sendo um passo muito importante para a Web!
Quais tipos de Request utilizando CORS?
CORS é amplamente implementado por diversos tipos de request e vou listar alguns deles:
- Utilizando
fetch
eXMLHttpRequest
utilizando CORS por natureza, mas podem ser configurado para não utilizar CORS caso necessário (não recomendado); - Com WebFonts quand utilizamos @fontFace no CSS (fiquei supreso também);
- WebGL texture utiliza de CORS por natureza
- Imagens e audios para canvas utilizam CORS quando utilizando:
drawImage()
; - CSS Shapes from images;
Visão geral do Funcionamento:
CORS trabalha enviando um "preflight" que é um novo header
enviado antes do seu request chegar no servidor. Assim o servidor vai checar esse novo header
que é chamado de OPTION
e vai verificar se é permitido aquela origem, porta, esquema, dentre outras coisas acessarem aquelas informações. HTTP requests que causam efeitos colaterais no servidor (geralmente métodos diferentes de GET
e POST
com certos tipos de MIME) EXIGÊM um "preflight" para o servidor verificar se realmente é permitido tais ações. CORS também incluí a informação que avisa ao cliente se as credenciais vão ou não serem enviadas.
Quando CORS passa por uma falha, os detalhes do erro não são mostrados por segurança, apenas sabemos que algo deu errado. Caso queira mais informações sobre o erro, pode olhar o console, que vai ter informações adicionais, mas não completas (por razões de segurança);
Utilizando CORS na prática
Os seguintes exemplos vou utilizar XMLHttpRequest
pois era assim que estava na documentação, então em casos de você procurar mais informações, não vai ter muita diferença. Vale lembrar que o mesmo vale para Fetch
O que é um simples request?
Antes de processeguir, é extremamente importante eu falar sobre este assunto, que é um simples requests.
O que é uma simples Request? É um request que não envia um "preflight" pois não é necessáro. Todas as médidas de seguranças são tomadas internamente pelo o navegador sem precisar de um "preflight".
Isso se dá porquê não tem a "necessidade". Utilizando métodos simples como GET
, POST
, o navegador inclui camadas de seguranças suficientes para nos proteger, diferente de request mais complexos que exigem um "preflight" com mais camada de segurança. Isso é uma otimização feita por parte dos navegadores e servidores.
Existem certos requesitos que fazem um request ser um semples request:
O método utilizado é um desses:
GET
POST
HEAD
Os cabeçalhos são limitados a:
- Accept
- Accept-Language
- Content-Language
- Content-Type (limited to certain values, such as application/x-www-form-urlencoded, - multipart/form-data, or text/plain)
O tipo de conteúdo permitido são:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
Se seu request cobre esses critérios, não vai ocorrer um "preflight", mas pode ficar tranquilo que o navegador irá incluir as camadas de seguranças necessárias.
Aqui está um exemplo com fetch
(apenas esse será com fetch
) para visualizar melhor:
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Accept': 'application/json',
},
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There was a problem with the fetch operation:', error);
});
Este código é um simples request, e cabe a você descobrir o porquê como exercícios :)
Agora vamos da uma olha em como seria um exemplo concreto de um simples request em prática.
Vamos supor que o https://foo.example
quer pegar algum conteúdo de: https://bar.other
const xhr = new XMLHttpRequest();
const url = "https://bar.other/resources/public-data/";
xhr.open("GET", url);
xhr.onreadystatechange = someHandler;
xhr.send();
Agora vamos dá uma olhada no que o navegador vai enviar para o servidor:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Note que a propriedade origin
mostra a URL que está fazendo o request. Agora vamos dá uma olhada na resposta do servidor:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
Vamos quebrar a resposta do servidor e vamos analisar como ele nos respondeu:
Primeiro, notamos que o servidores nos enviou a seguinte informação:Access-Control-Allow-Origin
com um valor de: *
o que significa que qualquer origem pode acessar as informações no servidor.
Access-Control-Allow-Origin: *
UMa coisa interessante sobre isso é que podemos limitar quem pode acessar as informações no servidor atráves desse paramêtro:
Access-Control-Allow-Origin: https://foo.example
Se fosse algo assim, https://bar.other
só aceitaria a origem https://foo.example
. Se fosse diferente, lançaria um erro e encerraria o request.
Quando utilizando credenciais a origem deve ser específicada obrigatoriamente e não pode conter o valor coringa
*
.
E aqui encerra sobre simples request. Vamos dá uma olhada agora em "preflight" request.
"preflight" Request
Ao contrário do simples request, um request com "preflight" é uma requisição que geralmente é mais complexa e possui mais camadas de segurança como consequência. Quando fazemos uma resquest "preflight", o navegador vai utilizar o método OPTION
para enviar um novo header
antes da requisição com o intuito de verificar se é seguro para enviar as informações.
O seguinte exemplo é um "preflight" request:
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://bar.other/doc");
xhr.setRequestHeader("X-PINGOTHER", "pingpong");
xhr.setRequestHeader("Content-Type", "text/xml");
xhr.onreadystatechange = handler;
xhr.send("<person><name>Arun</name></person>");
vamos dá uma olhada como funciona a comunicação entre o servidor e o cliente.
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Da linha 1 a 10 é o "preflight" request feito com o método OPTION
. O método OPTION
tem apenas como função enviar algumas informações para checkar a segurança da requisição, e não é possível alterar essas informações, assim garantido mais uma camada de segurança.
Quero dá ênfase nos dos seguintes paramêtros que aparecem nas linhas 9 e 10:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method
fala pro servidor que quando for aprovado o "preflight", o request vai ser feito usando o método POST
e nenhum outro além desse. Sendo uma camada de segurança evitando casos onde o criminoso possa interceptar o sinal e alterar para o método GET
e obter informações confidenciais.
Access-Control-Request-Headers
faça pro servidor que quando o "preflight" for aprovado, vai utilizar os headers: X-PINGOTHER
e Content-Type
custom headers. Agora o servidor tem a oportunidade de verificar se aceita tais condições.
Nas linhas 12 a 21 é a resposta do servidor que aprova o header e o método:
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
O servidor responde com: Access-Control-Allow-Origin: https://foo.example
que restrige o acesso e também informa que os método POST
, GET
e OPTIONS
são aceitos: Access-Control-Allow-Methods: POST, GET, OPTIONS
e por último envia os headers
aceitos que são: X-PINGOTHER, Content-Type
: Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Opa, não podemos esquecer do: Access-Control-Max-Age
que indica em segundos o tempo que uma resposta pode ser armazenada em cache, variando o valor padrão entre navegadores. Neste caso é 24 horas
UMa vez que tudo ocorre como esperado, o request ocorre:
POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some XML payload]
Request with credentials
Request com credenciais são diferentes em alguns aspectos. Por padrão, as credenciais nunca são incluídas, a menos que você as faça manualmente.
Neste exemplo o conteúdo original carregado é de https://foo.example
que faz uma requisição simples com o método GET
para https://bar.other
que seta cookies. O código ficaria mais ou menos assim:
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
Observe a propriedade: invocation.withCredentials = true;
que é responsável por enviar as credenciais indicado se é incluído ou não com um valor booleano. Se o servidor não responder com Access-Control-Allow-Credentials: true
as credenciais serão ignoradas.
Aqui está um exemplo da comunicação entre o servidor e client:
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: https://foo.example/examples/credential.html
Origin: https://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
Por mais que na linha 10 esteja explícido o armazenamento de cookies, se o servidor responder diferente de Access-Control-Allow-Credentials: true
, será ignorado.
As credenciais NUNCA serão incluídas com o método OPTION
, terá apenas: Access-Control-Allow-Credentials: true
para indicar que o request pode conter credenciais.
Alguns pontos que devemos nos ater ao trabalhar com credenciais:
- Access-Control-Allow-Origin deve ser uma específica e nunca uma coringa
*
por exemplo: Access-Control-Allow-Origin:https://example.com
- Access-Control-Allow-Headers deve ser uma específica e nunca uma coringa
*
por exemplo: Access-Control-Allow-Headers:X-PINGOTHER, Content-Type
- Access-Control-Allow-Methods deve ser uma específica e nunca uma coringa
*
por exemplo: Access-Control-Allow-Methods:POST, GET
- Access-Control-Expose-Headers deve ser uma específica e nunca uma coringa
*
por exemplo: Access-Control-Expose-Headers:Content-Encoding, Kuma-Revision
Caso tente enviar credenciais ou consultar e não compra uma dessas obrigações que citei acima, a o resquet vai ser bloqueado e lançara um erro.
Conclusão
Cometi algum equívoco? Me corrijam! Qualquer dúvida, fiquem a vontade para perguntar.