[REST API] 21. CORS

문정준's avatar
May 15, 2025
[REST API] 21. CORS
 

1. Origin / Remote Address

  • 클라이언트가 특정 서버에 request를 요청할 때에, 서버는 클라이언트의 IP 주소 (Host 주소, Remote Address)도 수집해야 하지만 제일 먼저 요청이 어디서 들어 왔는지 확인해야 함
  • 서버의 도메인 주소가 naver.com인데, 다른 사이트에서 naver.com에게 요청을 보내면 비정상적인 접근으로 판단하여 접근을 차단하기 때문
    • 요청을 보낸 근원지를 Origin이라고 하고, 이는 클라이언트의 IP 주소와 다름
  • 서버는 실제로 Remote Address 외에도 Origin, Referer (이전 주소), User-Agent, CSRF Token 등 요청이 안전하다는 증거를 입증 가능한 정보들을 받아야 함
    • Origin, Referer, User-Agent 등은 공격자가 임의로 조작이 가능하기 때문에, 추가로 필요한 검증용 토큰이 바로 CSRF (Cross-Site Request Forgery) Token
notion image

Referer

2. CSRF Defence

  • CSRF는 사용자의 인증 상태를 악용하여, 사용자가 의도하지 않은 요청을 보내도록 유도하는 공격
  • 피싱 사이트 등을 통해 인증 정보를 이용하여 공격하기 때문에 사용자와 서버 양 측에서는 이 요청이 올바른 요청인지, 악의를 가진 공격성 요청인지 알 수 없음
    • 이를 검증하기 위해 토큰(CSRF Token)을 사용
  • 토큰을 사용할 시 정상적인 서버에서는 세션을 통해 토큰을 지급하고 검증
    • 공격자는 토큰의 값을 알 수 없고, 위조할 수 없음
notion image
 

3. CORS (Cross Origin Resource Sharing)

  • CSRF 방어의 정책 때문에, HTTP 프로토콜은 같은 Origin이 아닌 곳의 요청을 기본적으로 전부 차단
  • 신뢰할 수 있는 특정 Origin의 요청을 열어주는 것이 바로 CORS (Cross Origin Resource Sharing)
notion image
  • Origin을 확인하기 위해서 클라이언트는 JS를 통한 요청 시 2번의 요청을 전송
    • 맨 먼저 전송하는 요청을 Preflight라고 함
    • Preflight에서 내가 전송할 요청의 정보를 알려주고, 서버에서 신뢰할 수 있어 요청을 허용하는 응답이 오면 본 요청을 다시 전송
    • Preflight가 거절되면 브라우저에서 에러 출력
notion image
notion image
 

4. 적용

  • 실제 운영할 Blog에 적용하기 위해 Filter를 사용
 
  • CorsFilter
    • Access-Control-Allow-Origin : 서버 측에서 접근을 허용하는 Origin 명시
    • Access-Control-Expose-Headers : 서버 측에서 응답에 남길 수 있는 Headers 명시
    • Access-Control-Allow-Methods : 서버 측에서 허용할 수 있는 Request Methods
      • Preflight는 Options라는 Method를 사용하므로 필수로 포함해야 함
    • Access-Control-Max-Age : 서버 측에서 CORS를 허용할 수 있는 시간 (초)
    • Access-Control-Allow-Headers : 서버 측에서 요청 받을 수 있는 헤더
      • Access-Control-Expose-Headers와 다름
    • Access-Control-Allow-Credentials : 서버에서 쿠키에 세션 값을 담는 것을 허용할 지에 대한 여부
@Slf4j public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String origin = request.getHeader("Origin"); log.debug("Origin : " + origin); response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); // response.setHeader("Access-Control-Expose-Headers", "Authorization"); // 응답 해줄 수 있는 헤더 response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); // OPTIONS : Preflight response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Key, Content-Type, Accept, Authorization"); // 요청 받을 수 있는 헤더 // X-~~~ : Customized Header response.setHeader("Access-Control-Allow-Credentials", "true"); // 쿠키 세션 값 허용 // Preflight 요청을 허용하고 바로 응답하는 코드 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(req, res); } } }
 
  • FilterConfig
@Bean public FilterRegistrationBean<CorsFilter> corsFilter() { FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CorsFilter()); registrationBean.addUrlPatterns("/*"); // 모든 요청에 적용 registrationBean.setOrder(1); // 필터 순서 설정 return registrationBean; }
 
 

Result

  • Cursor의 Live Server (127.0.0.1:5500)를 이용하여 Preflight가 승인되는 지 확인
Preflight 허용
Preflight 허용
Preflight Response Headers
Preflight Response Headers
Share article

sxias