장고(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('~~~')