본문으로 건너뛰기 CORS Complete Guide

CORS Complete Guide

CORS Complete Guide

이 글의 핵심

CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which origins can access your API. Understanding CORS is essential for building secure web applications.

Introduction

CORS (Cross-Origin Resource Sharing) is a security feature implemented by browsers to control how web pages can request resources from different origins.

What is an Origin?

An origin consists of:

  • Protocol: http or https
  • Domain: example.com
  • Port: :3000
https://example.com:3000
└─┬──┘ └────┬─────┘└┬─┘
  │         │       └─ Port
  │         └───────── Domain
  └─────────────────── Protocol

Same origin:

https://example.com/api/users
https://example.com/api/posts

Different origins (CORS applies):

http://localhost:3000  → http://localhost:8000
https://example.com    → https://api.example.com
https://example.com    → https://example.com:3000

1. The Problem

Without CORS, browsers block cross-origin requests:

// Frontend at http://localhost:3000
fetch('http://localhost:8000/api/users')
  .then(res => res.json())
  .catch(err => console.error(err));

// Error:
// Access to fetch at 'http://localhost:8000/api/users' 
// from origin 'http://localhost:3000' has been blocked by CORS policy

2. Basic Setup

npm install cors
const express = require('express');
const cors = require('cors');

const app = express();

// Enable CORS for all origins (not recommended for production)
app.use(cors());

app.get('/api/users', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

app.listen(8000);

3. Specific Origins

// Allow single origin
app.use(cors({
  origin: 'http://localhost:3000'
}));

// Allow multiple origins
const allowedOrigins = [
  'http://localhost:3000',
  'https://example.com',
  'https://www.example.com'
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (like mobile apps, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

4. Full Configuration

app.use(cors({
  origin: 'http://localhost:3000',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
  credentials: true, // Allow cookies
  maxAge: 3600, // Cache preflight response for 1 hour
}));

5. Credentials (Cookies)

// Server
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true, // Allow cookies
}));

app.get('/api/profile', (req, res) => {
  // Can now read cookies
  res.json({ user: req.user });
});
// Client
fetch('http://localhost:8000/api/profile', {
  credentials: 'include', // Send cookies
})
  .then(res => res.json());

6. Preflight Requests

For complex requests (PUT, DELETE, custom headers), browsers send a preflight OPTIONS request:

// Automatic preflight handling
app.use(cors());

// Manual preflight handling
app.options('/api/users', cors());

app.put('/api/users/:id', cors(), (req, res) => {
  // Update user
});

7. Dynamic Origins

app.use(cors({
  origin: (origin, callback) => {
    // Allow all subdomains
    if (!origin || origin.match(/\.example\.com$/)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

// Or check database
app.use(cors({
  origin: async (origin, callback) => {
    const allowed = await db.allowedOrigins.findOne({ origin });
    callback(null, !!allowed);
  }
}));

8. Route-Specific CORS

// Enable CORS for specific routes
app.get('/api/public', cors(), (req, res) => {
  res.json({ data: 'public' });
});

// No CORS for this route
app.get('/api/private', (req, res) => {
  res.json({ data: 'private' });
});

// Different CORS config per route
app.get('/api/admin', cors({
  origin: 'https://admin.example.com'
}), (req, res) => {
  res.json({ data: 'admin' });
});

9. Custom Headers

app.use(cors({
  origin: 'http://localhost:3000',
  allowedHeaders: [
    'Content-Type',
    'Authorization',
    'X-Requested-With',
    'X-Custom-Header'
  ],
  exposedHeaders: [
    'X-Total-Count',
    'X-Page-Count'
  ]
}));

// Client can now read exposed headers
fetch('http://localhost:8000/api/users')
  .then(res => {
    const totalCount = res.headers.get('X-Total-Count');
    return res.json();
  });

10. Environment-Based Configuration

const corsOptions = {
  origin: process.env.NODE_ENV === 'production'
    ? 'https://example.com'
    : 'http://localhost:3000',
  credentials: true,
};

app.use(cors(corsOptions));

11. Common Errors and Solutions

Error 1: No ‘Access-Control-Allow-Origin’ header

// Problem: CORS not enabled
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Solution: Add CORS
app.use(cors());

Error 2: Credentials not supported with ’*‘

// Problem: Can't use credentials with wildcard origin
app.use(cors({
  origin: '*',
  credentials: true, // ❌ Error!
}));

// Solution: Specify exact origins
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true, // ✅ Works
}));

Error 3: Method not allowed

// Problem: Method not in allowed methods
app.use(cors({
  methods: ['GET', 'POST'], // PUT not allowed
}));

app.put('/api/users/:id', (req, res) => {
  // CORS error!
});

// Solution: Add method
app.use(cors({
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

Error 4: Header not allowed

// Problem: Custom header not allowed
fetch('http://localhost:8000/api/users', {
  headers: {
    'X-Custom-Header': 'value' // Not allowed!
  }
});

// Solution: Add to allowedHeaders
app.use(cors({
  allowedHeaders: ['Content-Type', 'X-Custom-Header'],
}));

12. Production Configuration

const express = require('express');
const cors = require('cors');

const app = express();

// Production-ready CORS
const allowedOrigins = [
  'https://example.com',
  'https://www.example.com',
  'https://app.example.com',
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      console.warn(`CORS blocked: ${origin}`);
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
  credentials: true,
  maxAge: 86400, // 24 hours
  optionsSuccessStatus: 200,
}));

app.listen(process.env.PORT || 8000);

13. With Authentication

const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');

const app = express();

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// Protected route
app.get('/api/profile', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ user: decoded });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

// Client
fetch('http://localhost:8000/api/profile', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
  credentials: 'include',
});

14. CORS Proxy (Development)

For development, you can proxy requests:

// Next.js (next.config.js)
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:8000/api/:path*',
      },
    ];
  },
};

// Vite (vite.config.js)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
};

15. Testing CORS

// Test CORS with curl
curl -H "Origin: http://localhost:3000" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type" \
     -X OPTIONS \
     http://localhost:8000/api/users

// Check response headers
// Access-Control-Allow-Origin: http://localhost:3000
// Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE

Summary

CORS controls cross-origin access:

  • Security mechanism implemented by browsers
  • Origin checking prevents unauthorized access
  • Credentials support for cookies/auth
  • Preflight requests for complex operations
  • Production config must be restrictive

Key Takeaways:

  1. Never use origin: '*' in production
  2. Enable credentials only for trusted origins
  3. Specify allowed methods and headers
  4. Test CORS in production-like environment
  5. Use proxies for development

Next Steps:

Resources:


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Complete CORS guide for Node.js and Express. Learn what CORS is, how to configure it, handle preflight requests, and fix…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers CORS, Security, Node.js, Express, Backend, HTTP.