CORS en APIs con Go: Guía Completa
Imagina que tu API es un club exclusivo y CORS es el portero que decide quién puede entrar. Vamos a aprender a configurar este “portero” correctamente para mantener segura tu API mientras permitas el acceso a tus aplicaciones web.
¿Qué es CORS y por qué es importante?
CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad del navegador que controla cómo interactúan recursos de diferentes orígenes. Funciona mediante headers HTTP especiales.
Problema que resuelve:
Los navegadores bloquean por defecto solicitudes AJAX entre dominios diferentes (misma política de origen/Same-Origin Policy).
Ejemplo real:
Si tu frontend en https://tuaplicacion.com intenta llamar a tu API en https://api.tudominio.com, el navegador bloqueará la solicitud sin CORS.
Headers CORS esenciales
| Header | Ejemplo | Propósito |
|---|---|---|
Access-Control-Allow-Origin |
https://tuaplicacion.com |
Especifica qué dominios pueden acceder |
Access-Control-Allow-Methods |
GET, POST, PUT |
Métodos HTTP permitidos |
Access-Control-Allow-Headers |
Content-Type, Authorization |
Headers que puede enviar el cliente |
Access-Control-Allow-Credentials |
true |
Permite cookies/credenciales |
Access-Control-Max-Age |
3600 |
Cachear configuración CORS (segundos) |
Implementación básica en Go
1. Middleware CORS manual
package middleware
import "net/http"
func EnableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Configurar headers CORS
w.Header().Set("Access-Control-Allow-Origin", "https://tuaplicacion.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "3600")
// 2. Manejar preflight OPTIONS
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// 3. Pasar al siguiente handler
next.ServeHTTP(w, r)
})
}
2. Uso con gorilla/mux
package main
import (
"net/http"
"tu-proyecto/middleware"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
// Aplicar middleware CORS globalmente
r.Use(middleware.EnableCORS)
// Tus rutas
r.HandleFunc("/api/data", GetDataHandler).Methods("GET")
http.ListenAndServe(":8080", r)
}
Configuración avanzada
1. Múltiples orígenes permitidos
func EnableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Lista de orígenes permitidos
allowedOrigins := []string{
"https://tuaplicacion.com",
"https://app.tuclient.com",
"http://localhost:3000", // Para desarrollo
}
// Obtener origen de la solicitud
origin := r.Header.Get("Origin")
// Verificar si el origen está permitido
for _, allowed := range allowedOrigins {
if origin == allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
break
}
}
// Resto de la configuración...
})
}
2. Entornos de desarrollo vs producción
func getCORSConfig() map[string]string {
env := os.Getenv("APP_ENV") // "dev" o "prod"
if env == "dev" {
return map[string]string{
"Origin": "*",
"Methods": "*",
"Headers": "*",
"Credentials": "false",
}
}
return map[string]string{
"Origin": "https://tuaplicacion.com",
"Methods": "GET, POST, PUT, DELETE",
"Headers": "Content-Type, Authorization",
"Credentials": "true",
}
}
Solución de problemas comunes
1. Error: “No ‘Access-Control-Allow-Origin’ header”
Causa:
El origen no está incluido en los headers de respuesta.
Solución:
- Verifica el header
Originde la solicitud - Asegúrate que coincida exactamente con tus dominios permitidos
2. Error: “Credentials not supported”
Causa:
Usas Allow-Credentials: true pero Allow-Origin: *
Solución:
- Especifica dominios explícitos en lugar de
* - Ambos headers deben ser consistentes
3. Preflight OPTIONS falla
Causa:
No manejas correctamente las solicitudes OPTIONS.
Solución:
Implementa un handler para OPTIONS:
r.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
}).Methods("OPTIONS")
Mejores prácticas de seguridad
- No uses
Access-Control-Allow-Origin: *en producción - Limita métodos HTTP a solo los necesarios
- Especifica headers permitidos explícitamente
- Usa
Vary: Originpara evitar cacheo incorrecto - Implementa CSRF protection cuando uses credenciales
Configuración recomendada para producción
func ProductionCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validar origen contra lista permitida
if origin := r.Header.Get("Origin"); isValidOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
}
// Configuración estricta
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400") // 24 horas
// Headers de seguridad adicionales
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Pruebas de CORS
1. Usando cURL
# Solicitud simple
curl -I -X OPTIONS http://tuapi.com/endpoint \
-H "Origin: http://tuaplicacion.com" \
-H "Access-Control-Request-Method: POST"
# Deberías ver los headers CORS en la respuesta
2. Desde el navegador
// Prueba desde la consola del navegador
fetch('https://tuapi.com/data', {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS Error:', error));
Alternativas populares
1. Paquete rs/cors
import "github.com/rs/cors"
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://tuaplicacion.com"},
AllowedMethods: []string{"GET", "POST", "PUT"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
Debug: true, // Solo para desarrollo
})
handler := c.Handler(r)
http.ListenAndServe(":8080", handler)
}
2. Paquete gorilla/handlers
import "github.com/gorilla/handlers"
func main() {
corsHandler := handlers.CORS(
handlers.AllowedOrigins([]string{"https://tuaplicacion.com"}),
handlers.AllowedMethods([]string{"GET", "POST"}),
handlers.AllowedHeaders([]string{"Content-Type"}),
)
http.ListenAndServe(":8080", corsHandler(r))
}
Diagrama de flujo CORS
sequenceDiagram
participant Cliente as Cliente (navegador)
participant Servidor as API Go
Cliente->>Servidor: OPTIONS /data (Preflight)
Servidor->>Cliente: Headers CORS permitidos
Cliente->>Cliente: ¿Permitido? → Si/No
Cliente->>Servidor: GET /data (Real)
Servidor->>Cliente: Datos + Headers CORS
Casos de uso avanzados
1. Microservicios internos
// Permitir solo servicios dentro de tu red privada
func InternalServiceCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if isPrivateIP(ip) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
}
next.ServeHTTP(w, r)
})
}
2. APIs públicas
// Configuración para API pública
func PublicAPICORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Max-Age", "300")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Conclusión
Dominar CORS es esencial para construir APIs modernas y seguras. Recuerda:
- Configura orígenes permitidos explícitamente
- Maneja correctamente las preflight requests
- Usa middlewares para mantener tu código limpio
- Prueba exhaustivamente en diferentes entornos
- Sigue principios de seguridad mínimos privilegios
Con esta implementación tendrás un control granular sobre quién puede acceder a tu API desde el navegador, manteniendo la seguridad sin sacrificar funcionalidad.