CORS: o que é e como resolver o erro "blocked by CORS policy"
CORS é a regra do navegador por trás de uma das mensagens de erro mais conhecidas do desenvolvimento web. Todo desenvolvedor que conecta um front-end a uma API em outro endereço já viu a mensagem no console: "Access to fetch at ... from origin ... has been blocked by CORS policy". O erro confunde porque a mesma requisição funciona no Postman e no curl. A explicação é que o CORS é uma regra do navegador, e entender como ela funciona torna a solução simples e segura.
Primeiro: o que é uma origem
Uma origem é a combinação de protocolo + domínio + porta. São origens diferentes:
http://localhost:5173ehttp://localhost:8080(porta diferente);https://app.exemplo.comehttps://api.exemplo.com(subdomínio diferente);http://exemplo.comehttps://exemplo.com(protocolo diferente).
A política de mesma origem
Por segurança, o navegador impede que um script de uma origem leia as respostas de outra origem. Sem essa regra, um site malicioso aberto em uma aba poderia fazer requisições ao seu internet banking, usando os seus cookies, e ler os resultados.
O CORS (Cross-Origin Resource Sharing) é o mecanismo pelo qual o servidor diz ao navegador: "eu permito que esta outra origem leia as minhas respostas". Ou seja, quem libera o CORS é o servidor da API, nunca o front-end.
Por que funciona no Postman
Postman, curl e chamadas de servidor para servidor não são navegadores e não aplicam a política de mesma origem. Por isso a API responde normalmente. O bloqueio acontece apenas no navegador, que recebe a resposta mas não a entrega ao JavaScript.
Os cabeçalhos do CORS
O servidor responde com cabeçalhos como:
Access-Control-Allow-Origin: https://app.exemplo.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
A requisição preflight
Para requisições que não são "simples", como as que usam PUT ou DELETE, enviam Content-Type: application/json ou o cabeçalho Authorization, o navegador envia antes uma requisição OPTIONS, chamada preflight, perguntando se a requisição real é permitida. Se o servidor não responder ao OPTIONS com os cabeçalhos corretos, a requisição real nem é enviada.
Um erro muito comum: o OPTIONS chega em um filtro de autenticação, que exige token e responde 401 ou 403. O preflight falha e o navegador mostra erro de CORS, embora a causa real seja a autenticação. O preflight não envia o token; ele precisa ser liberado antes da autenticação.
Configurando no Spring Boot
Com Spring Security, configure o CORS na cadeia de segurança, para que o preflight seja tratado antes da autenticação:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.exemplo.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
Para criar a API em si, veja sua primeira API com Spring Boot. Se, depois do CORS, os POST passarem a retornar 403, o próximo suspeito é o CSRF; veja Spring Security 403 em POST.
Configurando no Node.js com Express
import express from "express";
import cors from "cors";
const app = express();
app.use(cors({
origin: ["https://app.exemplo.com"],
credentials: true,
}));
Em desenvolvimento: use o proxy
Durante o desenvolvimento, ferramentas como Vite e Create React App permitem configurar um proxy: o front-end chama /api na própria origem e o servidor de desenvolvimento repassa para a API. Como para o navegador tudo está na mesma origem, não há CORS envolvido. Exemplo no Vite:
// vite.config.js
export default {
server: {
proxy: { "/api": "http://localhost:8080" },
},
};
Erros que deixam a API insegura
- **
Access-Control-Allow-Origin: *com credenciais.** O navegador nem aceita essa combinação, mas tentar contornar refletindo qualquer origem recebida é ainda pior: qualquer site passa a poder ler respostas autenticadas. - Refletir o cabeçalho
Originsem validar contra uma lista de origens permitidas. - Liberar todas as origens em produção "porque funcionou".
- Usar extensões do navegador que desativam o CORS para "resolver". Isso esconde o problema só na sua máquina.
Use uma lista explícita de origens permitidas, separada por ambiente, e guarde-a em configuração, não no código. Veja variáveis de ambiente e segredos.
Como diagnosticar
- Abra as ferramentas de desenvolvedor (F12), aba Network.
- Procure a requisição OPTIONS (preflight), se houver, e veja o código de status e os cabeçalhos da resposta.
- Confira se
Access-Control-Allow-Origincontém exatamente a origem do front-end, incluindo protocolo e porta. - Se o OPTIONS retornar 401 ou 403, o problema está na ordem dos filtros de autenticação.
Para os códigos de resposta, veja códigos de status HTTP explicados.
Na prática da PruPru
Nem todo erro em requisição de SPA é CORS. Num sistema com Spring Boot 3 e React feito pela PruPru, as consultas funcionavam, mas todo POST, PUT e DELETE voltava 403, até o botão de sair. O navegador não mostrava erro de CORS: a causa era a proteção CSRF do Spring Security 6, que passou a esperar o token em outro formato. Antes de mexer em cabeçalhos de CORS, confira na aba Network se a resposta é um bloqueio do navegador ou um 403 do servidor. O caso completo está em Spring Security retornando 403 em POST com React.
Sistemas sob medida
A PruPru desenvolve APIs, sistemas web e integrações com as mesmas práticas descritas neste guia. Se a sua empresa precisa de software bem construído, conheça os serviços.
Ver serviços →Perguntas frequentes
Por que a requisição funciona no Postman e não no navegador?
Porque o CORS é aplicado apenas pelos navegadores. Postman e curl não seguem a política de mesma origem. O servidor responde normalmente, mas o navegador bloqueia o acesso do JavaScript à resposta.
Dá para resolver o CORS no front-end?
Não. Quem autoriza outras origens é o servidor da API, por meio dos cabeçalhos de resposta. No desenvolvimento, um proxy no servidor de desenvolvimento evita o problema porque tudo passa a estar na mesma origem.
O que é a requisição OPTIONS que aparece antes da minha?
É o preflight. O navegador pergunta ao servidor se a requisição real é permitida, informando o método e os cabeçalhos. O servidor precisa responder com os cabeçalhos de CORS adequados.
Usar Access-Control-Allow-Origin igual a asterisco é seguro?
Para APIs públicas sem autenticação por cookies, pode ser aceitável. Para APIs autenticadas, use uma lista explícita de origens permitidas. O asterisco não funciona com credenciais.