"CORS não é um bug, é uma feature de segurança. Entender CORS é entender como o navegador protege você de sites maliciosos." — #30DiasJava Security Notes

🎯 Por que CORS é Importante?

Imagine que você está logado no seu banco online (banco.com) e, ao mesmo tempo, visita um site malicioso (site-malicioso.com). Sem CORS, o site malicioso poderia fazer requisições para o banco.com usando seus cookies de autenticação e roubar seus dados. O navegador bloqueia isso por padrão.

Exemplo do mundo real: Quando você acessa uma API de um frontend React rodando em localhost:5173, o navegador bloqueia a requisição porque a API está em localhost:8080 (origem diferente). Isso é o CORS protegendo você. Mas para nossa aplicação funcionar, precisamos configurar o CORS corretamente.

No nosso projeto: Temos:

  • Frontend React rodando em http://localhost:5173
  • Backend Spring Boot rodando em http://localhost:8080
  • Mobile app (Expo) que também precisa acessar a API

Sem CORS configurado, o navegador bloqueia todas as requisições entre frontend e backend. Com CORS configurado corretamente, permitimos que apenas origens confiáveis acessem nossa API.

🎯 Objetivo do Day 22

O Day 22 do #30DiasJava implementou uma configuração completa de CORS que permite que frontend e mobile acessem a API de forma segura, bloqueando requisições de origens não autorizadas.

🛠️ O que foi implementado

✅ Checklist de Implementação

1️⃣ CORS Configuration com origins permitidas configuráveis 2️⃣ Métodos HTTP permitidos (GET, POST, PUT, DELETE, OPTIONS, PATCH) 3️⃣ Headers permitidos (Authorization, Content-Type, etc.) 4️⃣ Preflight caching (1 hora) para reduzir requisições OPTIONS 5️⃣ Integração com Spring Security no SecurityFilterChain

✅ CORS Configuration

  • Origins permitidas: Lista configurável de origens confiáveis
  • Métodos HTTP: GET, POST, PUT, DELETE, OPTIONS, PATCH
  • Headers permitidos: Authorization, Content-Type, etc.
  • Credentials: Suporte para cookies e autenticação
  • Preflight caching: Cache de requisições OPTIONS por 1 hora

✅ Perfis de Ambiente

  • Desenvolvimento: Permite todas as origens (para facilitar desenvolvimento)
  • Produção: Apenas origens específicas permitidas
  • Configuração via properties: Fácil de ajustar sem mudar código

✅ Integração com Spring Security

  • CORS no SecurityFilterChain: Integrado com autenticação JWT
  • Headers expostos: Authorization, X-Total-Count para paginação
  • Suporte a credentials: Cookies e tokens de autenticação

📊 Como CORS Funciona

O Problema

┌─────────────────────┐         ┌─────────────────────┐
│  Frontend           │         │  Backend API        │
│  localhost:5173     │ ──────X │  localhost:8080     │
│  (Origem A)         │ BLOQUEADO│  (Origem B)        │
└─────────────────────┘         └─────────────────────┘

Por quê bloqueado?

  • Navegador detecta que origem é diferente
  • Por segurança, bloqueia a requisição
  • Erro no console: "CORS policy blocked"

A Solução

┌─────────────────────┐         ┌─────────────────────┐
│  Frontend           │         │  Backend API        │
│  localhost:5173     │ ──────✓ │  localhost:8080     │
│  (Origem A)         │ PERMITIDO│  (Origem B)        │
│                     │         │  + CORS configurado │
└─────────────────────┘         └─────────────────────┘

Como funciona?

  1. Frontend faz requisição para backend
  2. Backend verifica se origem está na lista permitida
  3. Se sim, retorna headers CORS permitindo a requisição
  4. Navegador vê os headers e permite a requisição

🔍 Preflight Requests

Quando você faz uma requisição "complexa" (POST com JSON, DELETE, etc.), o navegador primeiro faz uma requisição OPTIONS para perguntar ao servidor: "Posso fazer esta requisição?"

1. Frontend: POST /api/familias (com JSON)
   ↓
2. Navegador: OPTIONS /api/familias (preflight)
   ↓
3. Backend: Responde com headers CORS permitindo
   ↓
4. Navegador: Agora faz o POST real

Por quê isso existe?

  • Segurança: Servidor pode negar requisições perigosas antes que aconteçam
  • Performance: Cache da resposta preflight por 1 hora (não precisa perguntar toda vez)

💻 Implementação

SecurityConfig.java

package com.adelmonsouza.desafio.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // Lista de origens permitidas (configurável via application.yml)
    @Value("${app.security.cors.allowed-origins:http://localhost:5173,http://192.168.0.104:8080}")
    private String allowedOrigins;

    /**
     * Configuração de CORS.
     * 
     * O que é CORS?
     * - Cross-Origin Resource Sharing
     * - Permite que frontend (origem diferente) acesse a API
     * - Bloqueia requisições de origens não autorizadas
     * 
     * Por quê precisamos?
     * - Frontend React roda em localhost:5173
     * - Backend Spring Boot roda em localhost:8080
     * - Navegador bloqueia requisições entre origens diferentes
     * - CORS diz ao navegador: "Esta origem é confiável"
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // Converter string de origens em lista
        List<String> origins = Arrays.asList(allowedOrigins.split(","));
        
        // Em desenvolvimento, permitir todas as origens
        // (útil para testar com mobile/Expo em diferentes IPs)
        if (isDevProfile() || origins.isEmpty() || origins.contains("*")) {
            configuration.addAllowedOriginPattern("*");  // Permite qualquer origem
        } else {
            // Em produção, apenas origens específicas
            configuration.setAllowedOrigins(origins);
        }
        
        // Métodos HTTP permitidos
        configuration.setAllowedMethods(Arrays.asList(
            "GET",    // Buscar dados
            "POST",   // Criar dados
            "PUT",    // Atualizar dados
            "DELETE", // Deletar dados
            "OPTIONS", // Preflight requests
            "PATCH"   // Atualização parcial
        ));
        
        // Headers permitidos
        configuration.setAllowedHeaders(Arrays.asList(
            "Authorization",  // Token JWT
            "Content-Type",   // Tipo de conteúdo (JSON, etc.)
            "X-Requested-With", // Identifica requisições AJAX
            "Accept",         // Tipos de resposta aceitos
            "Origin"          // Origem da requisição
        ));
        
        // Headers que o frontend pode ler
        configuration.setExposedHeaders(Arrays.asList(
            "Authorization",  // Token JWT (se necessário)
            "X-Total-Count"   // Total de registros (para paginação)
        ));
        
        // Permitir cookies e autenticação
        // IMPORTANTE: Só funciona se allowedOrigins for específico (não "*")
        configuration.setAllowCredentials(true);
        
        // Cache de preflight requests por 1 hora
        // Reduz número de requisições OPTIONS
        configuration.setMaxAge(3600L);
        
        // Aplicar configuração para todos os endpoints
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        
        return source;
    }

    /**
     * SecurityFilterChain - Integra CORS com autenticação.
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Desabilitar CSRF (APIs REST stateless com JWT não precisam)
            .csrf(csrf -> csrf.disable())
            
            // Configurar CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // ... outras configurações de segurança
            
        return http.build();
    }

    private boolean isDevProfile() {
        String activeProfiles = System.getProperty("spring.profiles.active", "");
        return Arrays.asList(activeProfiles.split(",")).contains("dev");
    }
}

application.yml

# Configuração de CORS
app:
  security:
    cors:
      # Lista de origens permitidas (separadas por vírgula)
      # Em desenvolvimento, pode usar "*" para permitir todas
      allowed-origins: |
        http://localhost:5173,
        http://localhost:3000,
        http://192.168.0.104:8080,
        https://enouveau.io,
        https://www.enouveau.io

📈 Exemplos de Requisições

Requisição Simples (GET)

Frontend → Backend:
GET /api/familias
Origin: http://localhost:5173

Backend → Frontend:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Content-Type: application/json

[... dados ...]

Requisição Complexa (POST com Preflight)

1. Preflight (OPTIONS):
Frontend → Backend:
OPTIONS /api/familias
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

Backend → Frontend:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

2. Requisição Real (POST):
Frontend → Backend:
POST /api/familias
Origin: http://localhost:5173
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{"nome": "Família Silva"}

Backend → Frontend:
HTTP/1.1 201 Created
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Content-Type: application/json

{"id": 1, "nome": "Família Silva", ...}

🧠 Insight Principal

"CORS não é um bug, é uma feature de segurança. Entender CORS é entender como o navegador protege você de sites maliciosos."

Por quê isso importa?

  1. Segurança

    • Sem CORS: Qualquer site pode fazer requisições, ataques CSRF, roubo de dados
    • Com CORS: Apenas origens autorizadas, navegador bloqueia requisições não autorizadas, proteção automática
  2. Compatibilidade com Frontend e Mobile

    • Sem CORS: Frontend React não funciona, mobile app não funciona, erro "CORS policy blocked"
    • Com CORS: Frontend funciona normalmente, mobile app funciona, requisições permitidas
  3. Flexibilidade

    • Desenvolvimento: Permite todas as origens (facilita testes)
    • Produção: Apenas origens específicas (segurança)

📈 Resultados Reais

  • Requisições bloqueadas: 0 (antes: 100% bloqueadas)
  • Frontend funcional: ✅ React consegue acessar API
  • Mobile funcional: ✅ Expo consegue acessar API
  • Segurança mantida: ✅ Apenas origens autorizadas

⚠️ Erros Comuns e Soluções

Erro: "CORS policy blocked"

Causa: Origem não está na lista permitida

Solução: Adicionar origem em application.yml:

app:
  security:
    cors:
      allowed-origins: http://localhost:5173,http://localhost:3000

Erro: "Credentials not allowed"

Causa: allowCredentials: true com allowedOrigins: ["*"]

Solução: Usar allowedOriginPatterns: ["*"] ou especificar origens:

// ❌ Não funciona
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowCredentials(true);

// ✅ Funciona
configuration.addAllowedOriginPattern("*");
configuration.setAllowCredentials(true);

// ✅ Ou melhor ainda (produção)
configuration.setAllowedOrigins(Arrays.asList("https://enouveau.io"));
configuration.setAllowCredentials(true);

Erro: "Preflight request failed"

Causa: Backend não está respondendo OPTIONS corretamente

Solução: Verificar se CORS está configurado no SecurityFilterChain:

http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

🎓 Lições Aprendidas

✅ O que funcionou bem

  • Configuração centralizada: Um único lugar para gerenciar CORS
  • Perfis de ambiente: Diferentes configurações para dev/prod
  • Preflight caching: Reduz requisições desnecessárias
  • Integração com Spring Security: Funciona perfeitamente com JWT

⚠️ Desafios e soluções

  • Mobile/Expo: Precisa permitir IPs dinâmicos em desenvolvimento
  • Credentials com wildcard: Não funciona, precisa especificar origens
  • Cache de preflight: Pode causar problemas em desenvolvimento (reduzir maxAge)

🏗️ Aplicação no Desafio das Águias

Como isso melhorou o produto:

  • Antes: Frontend e mobile não conseguiam acessar API, erro "CORS policy blocked"
  • Depois: Frontend React e mobile Expo funcionam perfeitamente, requisições permitidas de forma segura
  • Resultado: Sistema acessível de qualquer origem autorizada, segurança mantida

Código no projeto:

  • Arquivo: backend/src/main/java/com/adelmonsouza/desafio/config/SecurityConfig.java
  • Configuração: application.ymlapp.security.cors.allowed-origins
  • Branch: feature/day-22-cors-configuration

💬 Pergunta para Você

Como você configura CORS no seu projeto?

  • Usa configuração global ou por endpoint?
  • Como lida com preflight requests?
  • Qual foi o erro de CORS mais difícil de resolver?

Compartilhe suas experiências nos comentários! 👇

📚 Recursos

🔗 Links

Next episode → Day 23/30 — Security Best Practices