장고(Django) 개발: 폼(Form) 관리와 CSRF 취약점 해결

웨드_ |

장고에서는 HTML의 Form 기능을 forms라는 클래스로 제공한다. forms.Form을 상속받은 클래스가 하나의 폼이며, 변수들이 forms.각각_Field 클래스의 인스턴스가 되어 폼 내부의 엘리먼트(input 등)를 구성한다. 이는 모델을 만들때와 비슷한 형식이다.


작년에 장고(Django) 프레임워크로 프로젝트를 진행하면서 공부했던 개발 지식을 블로그에 다시 정리하려고 합니다. 개인 위키에 정리했던 것을 옮기는 수준이라, 친절한 설명은 기대하기 어렵고, 프로그래밍 언어와 파이썬 지식이 어느정도 있어야 이해할 수 있을 것입니다.


아래 내용들은 Django 1.6~1.7 버전을 기준으로하고, "쉽고 빠른 웹개발 Django"란 책에서 1/3, 공식 위키에서 1/3, 그리고 나머지는 구글링을 바탕으로 정리한 지식입니다. 말투가 존대와 반말이 섞여있어도 이해바랍니다 :)




1 사용자 정의 Form

2 Form 구현하기

3 CSRF 문제





1 사용자 정의 Form

보통 forms.py 파일에 forms.Form을 상속받아 폼 클래스를 정의함

폼의 필드 타입 및 필드 옵션

주요 필드 타입
설명
CharField문자열
IntegerField숫자
DateField날짜 (datetime.date객체)
DateTimeField일시 (datetime.datetime객체)
EmailField이메일 주소
URLFieldURL 주소
 모든 필드 타입

참고 : https://docs.djangoproject.com/en/1.7/ref/forms/fields/#built-in-field-classes

일반 필드 타입
설명
class BooleanField(**kwargs) 
class CharField(**kwargs) 
class ChoiceField(**kwargs) 
class TypedChoiceField(**kwargs) 
class DateField(**kwargs) 
class DateTimeField(**kwargs) 
class DecimalField(**kwargs) 
class EmailField(**kwargs) 
class FileField(**kwargs) 
class FilePathField(**kwargs) 
class FloatField(**kwargs) 
class ImageField(**kwargs) 
class IntegerField(**kwargs) 
class IPAddressField(**kwargs) 
class GenericIPAddressField(**kwargs) 
class MultipleChoiceField(**kwargs) 
class TypedMultipleChoiceField(**kwargs) 
class NullBooleanField(**kwargs) 
class RegexField(**kwargs) 
class SlugField(**kwargs) 
class TimeField(**kwargs) 
class URLField(**kwargs) 
class ComboField(**kwargs) 
class MultiValueField(fields=(), **kwargs) 
class SplitDateTimeField(**kwargs) 

 

참고 : https://docs.djangoproject.com/en/1.7/ref/forms/fields/#fields-which-handle-relationships

관계 필드 타입
 설명
 class ModelChoiceField(**kwargs) 
 class ModelMultipleChoiceField(**kwargs) 
 모든 필드 옵션

참고 : https://docs.djangoproject.com/en/1.7/ref/forms/fields/#module-django.forms.fields

모든 필드 옵션
설명
label필드명
required필수 입력
initial초기 값
widget필드가 HTML 표형되는 방법
help_text필드 설명 (아래 출력)
error_messages에러 발생시 메시지
validators검사 함수
localize데이터 로컬라이제이션

폼의 위젯

위젯은 폼 필드를 자세히 설정하기 위한 것으로, input 관련 엘리먼트의 속성을 사전형 데이터로 전달할 수 있음

주요 위젯 타입
설명
PasswordInput비밀번호 입력 필드 (type="password")
HiddenInput숨겨진 입력 필드 (type="hidden")
Textareatextarea (<textarea>)
FileInput파일 찾기 필드 (type="file")
 모든 위젯

참고 :  https://docs.djangoproject.com/en/1.7/ref/forms/widgets/#built-in-widgets

Input 관련 위젯
설명
TextInput 
NumberInput 
EmailInput 
URLInput 
PasswordInput 
HiddenInput 
DateInput 
DateTimeInput 
TimeInput 
Textarea 
Selector/Checkbox 관련 위젯
설명
CheckboxInput 
Select 
NullBooleanSelect 
SelectMultiple 
RadioSelect 
CheckboxSelectMultiple 
File upload 관련 위젯
설명
FileInput 
ClearableFileInput 
Composite 관련 위젯
 
MultipleHiddenInput 
SplitDateTimeWidget 
SplitHiddenDateTimeWidget 
SelectDateWidget 

폼의 주요 API

주요 API
설명
form.is_valid()폼의 입력값 올바른지
form.is_bound폼이 사용자 입력값을 가지고 있는지
form.data사용자가 입력한 폼 데이터
form.cleaned_data검사를 통과한 폼 데이터
Form.as_p()p태그로 폼 프린트
Form.as_ul()ul태그로 폼 프린트
Form.as_table()table태그로 폼 프린트

입력값 검사 방법

폼 클래스에 "clean_필드이름" 형식의 메서드를 추가하면, 입력값을 체크하여 검증된 입력값을 반환 하거나 올바르지 않을 경우, forms.ValidationError 예외를 발생 시킴.



2 Form 구현하기

  1. models.py에 폼으로 받을 데이터 정의
  2. forms.py에 사용자 정의 폼 클래스 정의
  3. views.py에 폼으로 입력받은 데이터를 어떻게 저장하고 보여줄지 정의
  4. templates에 폼의 템플릿 작성
  5. urls.py에 폼 템플릿의 url 설정

Tag를 입력받는 폼을 만드는 예제를 사용하여  구성

1.models.py에 폼으로 받을 데이터 정의

class Tag(models.Model):
    name = models.CharField(max_length=64, unique=True)
    bookmarks = models.ManyToManyField(Bookmark)
    def __unicode__(self):
        return self.name

2.forms.py에 사용자 정의 폼 클래스 정의

class BookmarkSaveForm(forms.Form):
    url = forms.URLField(
        label='주소',
        widget=forms.TextInput(attrs={'size'64})
    )
    title = forms.CharField(
        label='제목',
        widget=forms.TextInput(attrs={'size'64})
    )
    tags = forms.CharField(
        label='태그',
        required=False,
        widget=forms.TextInput(attrs={'size'64})
    )

3.views.py에 폼으로 입력받은 데이터를 어떻게 저장하고 보여줄지 정의

def bookmark_save_page(request):
    if request.method == 'POST':
        form = BookmarkSaveForm(request.POST)       
        if form.is_valid():
            # URL이 있으면 가져오고 없으면 새로 저장
            link, dummy = Link.objects.get_or_create(
                url=form.cleaned_data['url']
            )
             
            # 북마크가 있으며 가져오고 없으면 새로 저장
            bookmark, created = Bookmark.objects.get_or_create(
                user=request.user,
                link=link
            )
             
            #북마크 제목을 수정
            bookmark.title = form.cleaned_data['title']
             
            # 북마크를 수정한 경우, 이전에 입력된 모든 태그 삭제
            if not created:
                bookmark.tag_set.clear()
                 
            # 태그 목록 새로 만들기
            tag_names = form.cleaned_data['tags'].split()
            for tag_name in tag_names:
                tag, dummy = Tag.objects.get_or_create(name=tag_name)
                bookmark.tag_set.add(tag)
                 
            # 북마크 저장
            bookmark.save()
            return HttpResponseRedirect(
                '/user/%s' % request.user.username
            )
    else:
        form = BookmarkSaveForm()
         
    variables = RequestContext(request, {
        'form': form
    })
    return render_to_response('bookmark_save.html', variables)

4.templates에 폼의 템플릿 작성

{% block content %}
    <form method="post" action=".">
        {{ form.as_p }}
        <input type="submit" value="저장" />
    </form>
{% endblock %}

5.urls.py에 폼 템플릿의 url 설정

url(r'^tag/([^\s]+)/$', tag_page),



3 CSRF 문제

CSRF란 사이트 간 요청 위조(Cross-site request forgery)라는 웹사이트 취약점 공격 중에 하나를 뜻한다. 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.

장고는 1.2 버전부터 이러한 CSRF 취약점을 막는 기능인 CSRF 토큰방식을 기본으로 제공한다. 모든 POST 방식의 폼 전송에는 hidden 필드로 세션에 따른 임의 키값을 전송하며, 해당 키 값이 유효한지를 매번 확인한다

사용 방법

첫째로, settings.py의 미들웨어에 아래와 같이 추가한다.
(1.6~1.7버전은 프로젝트를 생성할 때 자동으로 입력되어 있다.)

MIDDLEWARE_CLASSES = (
    # ..
    'django.middleware.csrf.CsrfViewMiddleware',
    # ..
)

두번째로,  from 태그가 있는 템플릿에 {% csrf_token %}을 입력해야 한다.

<form method="post" action=".">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="저장" />
</form>

만일 미들웨어를 쓸 수 없는 경우라면, django.views.decorators.csrf 의 csrf_protect 장식자(decorator)를 쓸 수 도 있다.

from django.views.decorators.csrf import csrf_protect
from django.template import RequestContext
 
 
@csrf_protect
def view_function(request):
    con = {}
    # ..
    return render_to_response(
        'template.html',
        con,
        context_instance=RequestContext(request)
    )

특정 뷰에 대해 csrf를 적용하고 싶지 않다면 csrf_exampt 장식자를 사용한다.

from django.views.decorators.csrf import csrf_exempt
 
@csrf_exempt
def view_function(request):
    return HttpResponse('~~~')