CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 웹 애플리케이션에서 다른 도메인, 프로토콜 또는 포트에서 리소스를 요청할 때 발생하는 보안 메커니즘입니다. CORS는 보안상의 이유로 브라우저가 리소스를 요청할 때 발생하는 동일 출처 정책(Same-Origin Policy)을 우회할 수 있도록 합니다.
브라우저가 보안상의 이유로 한 출처(origin)에서 로드된 웹 페이지가 다른 출처(Origin)의 리소스에 액세스하는 것을 차단하는 정책
출처(origin) = 프로토콜 + 도메인 + 포트
ex) https://example.com:443과 https://example.com:80은 다른 출처로 간주(포트가 다름)
https://example.com과 http://example.com은 다른 출처(프로토콜이 다름)
-> 동일 출처 정책은 보안을 강화하지만, REST API나 외부 리소스를 사용해야 할 경우 불편을 초래합니다. 이를 해결하기 위해 CORS가 도입
CORS의 작동 방식
CORS는 서버와 클라이언트 간의 협력을 통해 이루어집니다. 클라이언트(웹 브라우저)가 다른 리소스를 요청하면, 서버는 요청을 허용하거나 차단합니다. 이를 위해 서버는 HTTP 응답 헤더에 특정 정보를 포함해야 합니다.
1) CORS 요청의 종류
CORS의 요청은 단순 요청(Simple Request)과 사전 요청(Preflight Request)로 나뉩니다.
단순 요청은 GET, POST, HEAD 메서드로 이루어진 요청으로 요청헤더가 다음의 조건을 만족해야 합니다.
Accept, Accept-Language, Content-Language, Content-Type
GET /resource HTTP/1.1
Host: api.example.com
Origin: https://example.com
서버는 응답 시 아래와 같은 CORS 허용 헤더를 포함
Access-Control-Allow-Origin: https://example.com
사전 요청은 단순 요청 이외의 요청(Put, DELETE 메서드, 커스텀 헤더 사용)은 브라우저가 먼저 서버에 HTTP OPTIONS 메서드로 사전 요청을 보냅니다.
서버는 이 요청에 대해 허용 여부를 응답해야 합니다.
OPTIONS /resource HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
서버의 응답:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-Custom-Header
2) CORS 관련 헤더
Origin: https://example.com
Access-Control-Allow-Origin : 요청을 허용할 출처를 명시. *를 사용하면 모든 출처 허용
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods : 허용할 HTTP 메서드 목록
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers : 클라이언트가 사용할 수 있는 요청 헤더를 지정.
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Allow-Credentials : 쿠키나 인증 정보를 포함할 수 있도록 설정. 반드시 출처를 특정해야 함(* 사용 불가)
Access-Control-Allow-Credentials: true
구현 예시
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 출처 허용
app.use(cors());
// 특정 출처만 허용
app.use(cors({ origin: 'https://example.com' }));
app.get('/data', (req, res) => {
res.json({ message: 'CORS is working!' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin(origins = "https://example.com")
public class MyController {
@GetMapping("/data")
public String getData() {
return "CORS is working!";
}
}
CORS에서 발생할 수 있는 문제
CORS가 필요한 이유
보안 강화 : 악의적인 스크립트가 사용자의 브라우저를 통해 무단으로 다른 도메인의 리소스를 가져오는 것을 방지
API 사용 : 클라이언트와 서버가 다른 출처에서 동작하는 현대적인 웹 애플리케이션에서 필수적
CORS(Cross-Origin Resource Sharing)에 대한 개념을 익힌 후, 신입 또는 취업 준비 중인 Java, Spring 백엔드 개발자 입장에서 실습할 만한 몇 가지 사례를 소개합니다. 이러한 실습은 실제 프로젝트 환경에서 발생할 수 있는 상황을 대비하며, 이론과 실무를 연결하는 데 큰 도움이 됩니다.
Spring Boot에서 CORS를 설정하고 테스트하는 간단한 실습입니다.
Spring Boot 애플리케이션을 생성한 후, CORS 설정을 추가합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 경로에 대해 CORS 허용
.allowedOrigins("http://localhost:3000") // 특정 출처만 허용
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드
.allowedHeaders("*") // 모든 헤더 허용
.allowCredentials(true); // 쿠키, 인증정보 허용
}
};
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "CORS Test Success!";
}
}
프론트엔드 코드 (React)
fetch('http://localhost:8080/test', {
method: 'GET',
credentials: 'include', // 쿠키 전송 포함
})
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
결과
"CORS Test Success!" 출력.Preflight 요청의 필요성과 작동 방식을 이해하기 위한 실습입니다.
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/data")
public String postData(@RequestHeader("X-Custom-Header") String header) {
return "Received header: " + header;
}
}
Preflight 요청을 발생시키는 프론트엔드 코드
fetch('http://localhost:8080/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'TestHeader', // Custom 헤더 추가
},
body: JSON.stringify({ message: 'Hello' }),
})
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
결과
쿠키와 같은 인증 정보를 포함한 요청에서 CORS가 어떻게 작동하는지 실습합니다.
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
@RestController
public class CookieController {
@GetMapping("/set-cookie")
public String setCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("myCookie", "cookieValue");
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
return "Cookie set!";
}
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(value = "myCookie", defaultValue = "NoCookie") String cookie) {
return "Cookie value: " + cookie;
}
}
allowCredentials(true)를 반드시 설정합니다.
쿠키 설정 요청
fetch('http://localhost:8080/set-cookie', {
method: 'GET',
credentials: 'include', // 쿠키 전송 허용
});
쿠키 확인 요청
fetch('http://localhost:8080/get-cookie', {
method: 'GET',
credentials: 'include', // 쿠키 전송 허용
})
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
결과
*와 특정 출처 설정, 메서드 및 헤더 제한 등을 변경하여 정책의 효과 비교.궁금한 점이 있거나 추가 실습 아이디어가 필요하면 언제든 질문하세요! 😊