[개발일지 #2] CSRF 토큰이 날 괴롭힌다..

김유진·2022년 7월 29일
11

React

목록 보기
20/64

이번에 카카오 로그인을 구현하기 위하여 백엔드와 프론트엔드 소통에 대해서 열심히 공부하고 있다.
서로 아이피 주소를 공유하여 post 요청을 통하여 인가코드를 백엔드에 보내주려고 하였으나...
자꾸 403 Forbidden 에러가 뜨면서 전송이 실패한다!!!

그 이유를 살펴보던 중, 장고에서는 서버가 공격받는것을 방지하기 위하여 CSRF 토큰이 존재하는 이들의 POST 요청을 받아들이고, 그 이외에는 보안을 위하여 권한을 허락하지 않는다고 한다.

나는 Axios로 인가코드를 보내야하는 입장이기 때문에 CSRF 토큰을 같이 Header나 Post 명령어에 포함하여 전달해야 하는 입장이다.
오늘 오후 3시에 백엔드팀을 만나서 더 시도해보기로 하였는데, 이 상황을 해결하기 위하여 내가 찾아본 내용을 정리하려고 한다.

1. CSRF이란?

CSRF는 Cross Site Request Forgery로, 다른 사이트에서 유저가 보내는 요청을 조작하는 공격이다!
예시로는 이메일에 첨부된 링크를 누르면 내 은행계좌의 돈이 빠져나가는 방식의 해킹 등이 있다.
클라이언트가 자신이 하고 싶은 행동에 대한 명령을 내렸는데, 해커가 페이지를 바꿔치기 하는등의 방법으로 해킹을 하기도 하기 때문에 이를 방지하기 위해, CSRF 토큰이 존재하는 것이다.

CSRF 토큰은 서버에 들어온 요청이 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰이다.

2. CSRF와 장고

나는 개발에 Django 백엔드와 함께하기 때문에 장고에서 어떻게 CSRF 토큰을 다루는지 확인할 필요가 있었다.
아래는 장고 공식 문서인데, 가이드라인을 확인하면서 필요한 부분만 쏙쏙 뽑아 보도록 해보자.
https://docs.djangoproject.com/en/3.0/ref/csrf/

위에서 이야기하고 있는 내용을 대략적으로 요약하자면 아래와 같다.

  1. 뷰에서 템플릿을 만들어 넘길때, CSRF 토큰값을 쿠키에 담아서 보내준다.
  2. django 프레임워크에서 설정한 헤더와 쿠키로 CSRF 토큰을 넘기면 된다. (이걸 프론트엔드에서 넘겨주어야 함)

settings.py에 아래 코드를 추가한다.

.....

CORS_ORIGIN_ALLOW_ALL = True

CORS_ALLOW_CREDENTIALS = True

CSRF_TRUSTED_ORIGINS = (
    'localhost:8000',
    '127.0.0.1:8000',
)

CORS_ORIGIN_WHITELIST = (
    'localhost:8000',
      '127.0.0.1:8000',
)

CORS_ALLOW_HEADERS = (
    'access-control-allow-credentials',
    'access-control-allow-origin',
    'access-control-request-method',
    'access-control-request-headers',
    'accept',
    'accept-encoding',
    'accept-language',
    'authorization',
    'connection',
    'content-type',
    'dnt',
    'credentials',
    'host',
    'origin',
    'user-agent',
    'X-CSRFToken',
    'csrftoken',
    'x-requested-with',
)

....

헤더이름은 자유롭게 설정할 수 있다. 다만, 장고 프레임워크에서 CSRF 헤더이름과 CSRF 쿠키의 이름이 위 처럼 지정되어 있어 이대로 진행한다.

그리고 이것이 등장할 스크립트에 이렇게 작성해주면 되는데, 나는 React앱의 index.js에 다음과 같이 추가할 것이다.

axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFToken"
axios.defaults.headers.common['X-CSRFToken'] = getCookie("csrftoken");

자 그럼 이제 React에서 axios를 통하여 POST 요청을 보낼 때 CSRF TOKEN 을 헤더에 추가해서 통신을 시도해야 한다.

3. CSRF와 Axios

먼저 이 부분에 대해서 알아보기 전에 내가 HTTP 통신에서 가장 헷갈렸던 것이 바로 Header이다.
먼저 HTTP의 구조를 정리하고 이것이 어떤 역할을 가지고 있는지 파악해보고자 한다.

HTTP의 구조

HTTP는 헤더와 본문으로 구성되어 있다. HTTP 본문에는 실제로 통신과정에서 주고 받을 컨텐츠가 담겨져 있다.
HTTP헤더는 HTTP메시지(요청/응답)와, 본문에 대한 정보를 말해주고 있다. 조금 더 자세히 말하자면, 헤더는 해당 메시지가 제공하는 기능에 대한 최소한의 정보가 정리된 요약본이라고 할 수 있기 때문이다. 헤더에 불필요한 내용을 담으면 네트워크로 전송되는 데이터의 크기가 커져서 빠른 전송이 불가능하기 때문에 프로토콜을 설계할 때부터 꼭 필요한 내용만 담아야 하고, 모든 기능이 표현되어야 한다.


헤더의 종류에는 이렇게 세가지가 존재한다.

1) General Header

전송되는 컨텐츠에 대한 정보보다는, 요청/응답이 이루어지는 날짜 및 시간등에 대한 일반적인 정보가 포함된다.

  • Date : 현재시간 (Sat, 25 May 2022 GMT)

  • Cache-Control : 캐시 제어+ no-cache : 모든 캐시를 쓰기 전에 서버에 해당 캐시를 사용해도 되는지 확인하겠다.
    - public : 공유 캐시에 저장해도 된다.
    - max-age : 캐시의 유효시간을 명시하겠다.
    - private : '브라우저' 같은 특정 사용자 환경에만 저장하겠다.
    - must-revalidate : 만료된 캐시만 서버에 확인하겠다.
    - no-store : 캐시를 저장하지 않겠다.

  • Transfer-Encoding : body 내용 자체 압축 방식 지정본문에 데이터 길이가 나와서 브라우저가 해석해서 화면에 뿌려줄 때 이 기능을 사용한다. 'chunked'면 본문의 내용이 동적으로 생성되어 길이를 모르기 때문에 나눠서 보낸다는 의미다.

  • Upgrade : 프로토콜 변경시 사용 ex) HTTP/2.0

  • Via : 중계(프록시)서버의 이름, 버전, 호스트명

  • Content-Encoding : 본문의 리소스 압축 방식 (transfer-encoding은 body 자체이므로 다름)

  • Content-type : 본문의 미디어 타입(MIME) ex) application/json, text/html

  • Content-Length : 본문의 길이

  • Content-language : 본문을 이해하는데 가장 적절한 언어 ex) ko 한국사이트여도 본문을 이해하는데 영어가 제일 적절하면 영어로 지정된다.

  • Expires : 자원의 만료 일자

  • Allow : 사용이 가능한 HTTP 메소드 방식 ex) GET, HEAD, POST

  • Last-Modified : 최근에 수정된 날짜

  • ETag : 캐시 업데이트 정보를 위한 임의의 식별 숫자

  • Connection : 클라이언트와 서버의 연결 방식 설정 HTTP/1.1은 kepp-alive 로 연결 유지하는게 디폴트.

    2) Request/ Response Header

    Request Header는 웹브라우저가 웹서버에 요청하는 것을 텍스트로 변환한 메시지들이다.

    request header form

  • Request Line : 어떤 웹서버로 접속(Host 부분)해서, 어떠한 방식(HTTP/1.1)으로, 어떠한 메소드(GET)를 통해 무엇을(/doc/test/.html) 요청했는지에 대한 메시지가 담겨있다.

  • Host : 요청하려는 서버 호스트 이름과 포트번호

  • User-agent : 클라이언트 프로그램 정보 ex) Mozilla/4.0, Windows NT5.1 이 정보를 통해서 서버는 클라이언트 프로그램(브라우저)에 맞는 최적의 데이터를 보내줄 수 있다.

  • Referer : 바로 직전에 머물렀던 웹 링크 주소(해당 요청을 할 수 있게된 페이지)

  • Accept : 클라이언트가 처리 가능한 미디어 타입 종류 나열 ex) / - 모든 타입 처리 가능, application/json - json데이터 처리 가능.

  • Accept-charset : 클라이언트가 지원가능한 문자열 인코딩 방식

  • Accept-language : 클라이언트가 지원가능한 언어 나열

  • Accept-encoding : 클라이언트가 해석가능한 압축 방식 지정 ex) gzip, deflate 압축이 되어있다면 content-length와 content-encoding으로 압축을 해제한다.

  • Content-location : 해당 개체의 실제 위치

  • Content-disposition : 응답 메세지를 브라우저가 어떻게 처리할지 알려줌. ex) inline, attachment; filename='jeong-pro.xlsx'

  • Content-Security-Policy : 다른 외부 파일을 불러오는 경우 차단할 리소스와 불러올 리소스 명시ex) default-src 'self' -> 자기 도메인에서만 가져옴 ex) default-src 'none' -> 외부파일은 가져올 수 없음 ex) default-src https -> https로만 파일을 가져옴

  • If-Modified-Since : 여기에 쓰여진 시간 이후로 변경된 리소스 취득. 페이지가 수정되었으면 최신 페이지로 교체하기 위해 사용된다.

  • Authorization : 인증 토큰을 서버로 보낼 때 쓰이는 헤더

  • Origin : 서버로 Post 요청을 보낼 때 요청이 어느 주소에서 시작되었는지 나타내는 값
    이 값으로 요청을 보낸 주소와 받는 주소가 다르면 CORS 에러가 난다.

  • Cookie : 쿠기 값 key-value로 표현된다. ex) attr1=value1; attr2=value2

Response Header는 반대로 웹서버가 웹브라우저에 응답하는 콘텐츠가 들어가있는 메시지이다.

response header form

: 웹브라우저가 요청한 메시지에 대해서 status 즉 성공했는지 여부(202, 400 등), 메시지, 그리고 요청한 응답 값들이 body에 담겨있다.

  • Location : 301, 302 상태코드일 떄만 볼 수 있는 헤더로 서버의 응답이 다른 곳에 있다고 알려주면서 해당 위치(URI)를 지정한다.
  • Server : 웹서버의 종류 ex) nginx
  • Age : max-age 시간내에서 얼마나 흘렀는지 초 단위로 알려주는 값
  • Referrer-policy : 서버 referrer 정책을 알려주는 값 ex) origin, no-referrer, unsafe-url
  • WWW-Authenticate : 사용자 인증이 필요한 자원을 요구할 시, 서버가 제공하는 인증 방식
  • Proxy-Authenticate : 요청한 서버가 프록시 서버인 경우 유저 인증을 위한 값

* HTTP 메소드에서 가장 자주 쓰이는 GET과 POST 비교

A. GET

우선 GET 방식은 요청하는 데이터가 HTTP Request Message의 Header 부분의 url 에 담겨서 전송된다. 때문에 url 상에 ? 뒤에 데이터가 붙어 request 를 보내게 되는 것이다.(이것이 parameter) 이러한 방식은 url 이라는 공간에 담겨가기 때문에 전송할 수 있는 데이터의 크기가 제한적이다. 또 보안이 필요한 데이터에 대해서는 데이터가 그대로 url 에 노출되므로 GET방식은 적절하지 않다. (ex. password) 쿼리스트링으로 보내는것 자제하기...

B. POST

POST 방식의 request 는 HTTP Message의 Body 부분에 데이터가 담겨서 전송된다. 때문에 바이너리 데이터를 요청하는 경우 POST 방식으로 보내야 하는 것처럼 데이터 크기가 GET 방식보다 크고 보안면에서 낫다.(하지만 보안적인 측면에서는 암호화를 하지 않는 이상 고만고만하다.)

아래는 실제로 작성된 HTTP의 헤더 부분이다.

Get /test/test.htm HTTP/1.1
Accept: */*
Accept-Language: ko
Accept-Encoding: gzip, deflate
If-Modified-Since: Fri, 21 Jul 2006 05:31:13 GMT
If-None-Match: "734237e186acc61:a1b"
User-Agent: Mozilla/4.0(compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1)
Host: localhost
Connection: Keep-Alive

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1X-Powered-By: ASP.NET
Date: Fri, 21 Jul 2006 05:32:01 GMT
Content-Type: text/html
Accept-Ranges: bytesLast-Modified: Fri, 21 Jul 2006 05:31:52 GMTE
Tag: "689cb7f885acc61:a1b"
Content-Length: 101

4. Axios에서 Header 작성해서 토큰 넘겨보기

axios.post('url', {"body":data}, {
    headers: {
    'Content-Type': 'application/json'
    }
  }
)

일단 axios를 이용하여 post로 데이터와 헤더를 넘겨주는 것은 다음과 같은 형식으로 작성할 수 있다.

실습 진행해보기

  • 백엔드에서 쿠키로 CSRF 토큰을 발급해준다.
  • 프론트엔드에서 그 쿠키를 가지고 axios 호출시에 Header로 보낸다.

먼저 각 서버에 설정되어있는 기본 세팅값이다.

서로 기본 세팅값이 다르다. 그렇기 때문에 axios의 설정을 django에 맞게 맞춘다.

axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'

서버는 인증된 사용자에게 csrftoken을 setCookie의 형태로 response header에 담아 전송한다. 이를 받은 클라이언트는 이후부터 자신의 header와 cookie에 토큰을 담은 채로 api call을 하게 되고 서버는 이 토큰이 헤더에 포함되어 있는 경우에만 DB의 수정을 허가한다.

이 두 코드는 csrf token을 담을 cookie의 이름과 header의 이름을 설정해주는 코드다.
이 코드는 settings.py의 이름과 일치하여야 한다. 이 이름(csrftoken, X-CSRFToken)은 django의 기본 세팅에 맞추어 적은 것이다.

settings.py의 세팅을 axios와 합이 같게 만든다.

CSRF_COOKIE_NAME = 'XSRF-TOKEN'
CSRF_HEADER_NAME = 'X-XSRF-TOKEN'

https://hashcode.co.kr/questions/10418/djangoaxios-csrf-403%EC%97%90%EB%9F%AC-%EC%A7%88%EB%AC%B8%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4

https://www.techiediaries.com/django-react-forms-csrf-axios/

1개의 댓글

comment-user-thumbnail
2022년 11월 23일

잘 보고 갑니다~

답글 달기