Django 기준으로 웹 보안 및 토큰 개념 정리

문승기·2025년 6월 1일
0

Django 보안 구조 정리: JWT + CSRF + CORS


1. CSRF 공격이란?

개념

CSRF(Cross-Site Request Forgery)는 인증된 사용자의 권한을 이용해 의도하지 않은 요청을 서버에 강제로 수행하게 만드는 공격이다. 즉, 사용자가 원치 않는데도 어떤 요청이 자동으로 실행되는 상황으로 이해할 수 있다.

공격 시나리오 예시

<img src="https://yourbank.com/transfer?amount=100000&to=hacker" />

→ 사용자가 로그인된 상태에서 위와 같은 공격자의 페이지에 접속하면, 쿠키가 자동 전송되면서 서버는 이를 정상 요청으로 처리하게 된다.

방지 방법

  • CSRF 토큰 발급 및 검증
  • Referer 헤더 검증
  • SameSite 쿠키 속성 설정

2. CORS란?

개념

CORS(Cross-Origin Resource Sharing)는 서로 다른 출처(origin) 간의 요청을 브라우저가 허용하도록 만드는 보안 정책이다. JavaScript에서 외부 API를 호출할 때 흔히 마주치는 이슈이다.

발생 조건 예시

  • 프론트엔드: http://localhost:3000
  • 백엔드(Django): http://localhost:8000

→ 도메인, 포트, 프로토콜 중 하나라도 다르면 브라우저가 요청을 차단한다.

해결 방법

  • django-cors-headers 라이브러리 설치
  • 서버 응답에 Access-Control-Allow-Origin, Access-Control-Allow-Credentials 헤더 설정

3. CSRF vs CORS 차이

항목CSRFCORS
대상사용자의 인증된 세션다른 출처에서 온 JS 요청
발생 위치서버브라우저
방지 목적악의적 요청 차단출처 제한
Django 대응{% csrf_token %} / X-CSRFTokendjango-cors-headers 설정

예시 비교

⚠️ 참고: 본 서비스는 Django가 HTML, 정적 파일, API를 모두 처리하는 단일 백엔드 구조이므로, CORS 설정은 불필요하다.

CSRF 예시

<img src="https://bank.example.com/withdraw?amount=100000" />

→ 로그인된 사용자가 외부 사이트를 통해 원치 않는 요청을 하게 되는 상황이다.

CORS 예시

fetch("http://localhost:8000/api", {
  method: "GET",
  credentials: "include"
});

→ 프론트엔드와 백엔드가 다른 출처에 있을 경우, 브라우저가 요청을 차단한다. 이 경우 백엔드에서 CORS 허용 설정이 필요하다.


4. django-cors-headers 적용법

pip install django-cors-headers
INSTALLED_APPS = ['corsheaders', ...]
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "https://yourfrontend.com"
]

5. JWT만으로 CSRF 방지 가능할까?

저장 위치CSRF 방지설명
localStorageOJS에서 직접 헤더에 넣기 때문에 CSRF 방지 가능 (단, XSS 위험 높음)
HttpOnly 쿠키X자동 전송되기 때문에 CSRF에 취약하다

✅ 정리: JWT를 HttpOnly 쿠키에 저장하는 경우, 별도의 CSRF 방어 로직이 반드시 필요하다.


6. 세션 기반 인증 vs JWT 기반 인증

항목세션 인증JWT 인증
저장 위치서버 세션클라이언트 쿠키/스토리지
확장성낮음높음
CSRF 대응필요 (쿠키 기반)필요 (HttpOnly 쿠키일 경우)
재발급서버에서 관리Refresh Flow 직접 구현 필요

7. Secure / HttpOnly / SameSite 쿠키 옵션 정리

옵션설명보안 목적
SecureHTTPS에서만 쿠키 전송네트워크 도청 방지
HttpOnlyJS 접근 불가XSS 방지
SameSite타 도메인 요청 제한CSRF 방지

SameSite 옵션:

  • Strict: 완전 차단 (가장 강력함)
  • Lax: 대부분의 GET 요청 허용
  • None: 외부 요청 허용 (Secure 옵션 필수)

8. CSRF 예외가 필요한 상황과 대안

예외 적용 상황

  • 외부 Webhook 수신 (예: 결제 서비스 등)
  • 테스트용 로컬 환경
  • 인증이 필요 없는 공용 API

적용 예시

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook(request):
    ...

대안 방안

  • JWT 또는 API Key 인증
  • IP 화이트리스트 적용
  • Referer 검증

9. {% csrf_token %}을 통한 CSRF 방어

Form 기반 예시

<form method="POST" action="/submit/">
  {% csrf_token %}
  <input name="email">
</form>

AJAX 기반 예시

headers: {
  "X-CSRFToken": getCookie("csrftoken")
}

10. CSRF 토큰이란?

  • Django에서 get_token() 또는 {% csrf_token %}으로 발급한다.
  • 클라이언트는 form 필드나 X-CSRFToken 헤더로 토큰을 전송한다.
  • 서버는 쿠키와 요청 값을 비교하여 요청의 정당성을 검증한다.

11. 우리 서비스의 JWT + CSRF 적용 전략

전제 조건

  • JWT: HttpOnly, Secure, SameSite=Strict 설정된 쿠키에 저장한다.
  • CSRF 토큰은 일반 쿠키(csrftoken)에 저장하며 JS에서 읽을 수 있도록 한다.
  • 모든 상태 변경 요청은 X-CSRFToken 헤더에 포함시켜야 한다.

1) 회원가입 시

회원가입은 대부분 인증이 필요 없는 공개 요청이지만, CSRF 공격을 방지하기 위해 반드시 CSRF 토큰이 포함되어야 한다. 서버가 렌더링한 템플릿에 {% csrf_token %}을 포함하면, 이 토큰이 csrftoken 쿠키로 전달된다. 이후 JavaScript에서 이 토큰을 읽어 AJAX 요청 시 헤더로 함께 전송해야 한다.

<!-- 서버에서 렌더링되는 HTML 템플릿 -->
<form id="signup-form">
  {% csrf_token %}
  <input type="text" name="email" placeholder="이메일">
  <input type="password" name="password" placeholder="비밀번호">
  <button type="submit">가입</button>
</form>
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

$("#signup-form").on("submit", function(e) {
  e.preventDefault();
  const email = $("input[name='email']").val();
  const password = $("input[name='password']").val();

  $.ajax({
    type: "POST",
    url: "/register/",
    headers: { "X-CSRFToken": getCookie("csrftoken") },
    data: { email, password },
    success: function(response) {
      alert("회원가입 성공!");
    },
    error: function(error) {
      alert("오류 발생");
    }
  });
});

2) 로그인 요청 시

로그인 요청 시에는 서버가 사용자의 자격을 확인하고 JWT를 발급한다. 이 JWT는 HttpOnly 쿠키에 저장되며, 클라이언트가 직접 접근할 수 없다. 동시에 서버는 CSRF 토큰도 함께 쿠키로 내려보낸다. 이 설정이 완료되면 이후의 모든 상태 변경 요청에서 CSRF 검증을 수행할 수 있다.

from django.middleware.csrf import get_token

response = JsonResponse({"message": "로그인 성공"})
response.set_cookie("access_token", jwt_token, httponly=True, secure=True, samesite='Strict')
response.set_cookie("csrftoken", get_token(request), httponly=False, secure=True, samesite='Strict')
return response

3) 로그인 이후 페이지 이동

로그인 이후에는 사용자가 페이지를 이동하거나 AJAX 요청을 보낼 수 있다. 이때 JWT는 자동으로 쿠키에 포함되어 전송된다. 다만, CSRF 방지를 위해 JavaScript에서는 쿠키에서 csrftoken을 읽어 요청 헤더에 포함시켜야 한다.

$.ajaxSetup({
  beforeSend: function(xhr) {
    xhr.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
  }
});

4) GET 요청 시

GET 요청은 리소스를 조회하는 용도이기 때문에 일반적으로 상태 변경을 일으키지 않는다. 따라서 CSRF 검증은 필요하지 않다. 다만, 인증이 필요한 경우 JWT 쿠키가 자동으로 포함되어야 한다.

fetch("/api/profile", {
  method: "GET",
  credentials: "include"
});

5) POST 요청 시

POST 요청은 데이터를 생성하거나 수정하는 요청이기 때문에 반드시 CSRF 토큰이 필요하다. 템플릿 기반이라면 {% csrf_token %}을 form 내부에 포함하고, JavaScript 기반 요청에서는 쿠키에서 csrftoken을 읽어 X-CSRFToken 헤더로 함께 전송해야 한다.

템플릿 기반 form 요청

<form method="POST" action="/submit/">
  {% csrf_token %}
  <input name="bio">
</form>

JavaScript 기반 요청

fetch("/submit/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRFToken": getCookie("csrftoken")
  },
  body: JSON.stringify({ bio: "소개글입니다" })
});

profile
AI 모델을 개발하여 이를 홀용한 서비스를 개발하고 운영하는 개발자가 되기 위해 꾸준히 노력하겠습니다!

0개의 댓글