์์ฆ ํ์ด์ฌ์ ์ด์ฉํ ํด๋ฆฐ์ฝ๋๋ฅผ ์ํ ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ ์ฑ ์ ๋ฐ๋ผํด๊ฐ๋ฉฐ TDD๋ก Django๋ฅผ ๋ค๋ฃจ๋ ๋ฒ์ ๋ฐฐ์๊ฐ๊ณ ์๋ค.
์ด์ ๋ ์
๋ ฅ ํผ์ ๋ง๋๋ ๋ถ๋ถ์ ๋ง๋ค๊ณ ํ
์คํธํด๋ณด์๋๋ฐ ์๊พธ ์คํจ๋ฅผ ํ๋ค.
์ด์ ๋ form
์์ csrf token ๋๋ฌธ์ด์๋ค.
CSRF๋ ์น์ฌ์ดํธ ์ทจ์ฝ์ ๊ณต๊ฒฉ์ ํ๋๋ก, ์ฌ์ฉ์๊ฐ ์์ ์ ์์ง์๋ ๋ฌด๊ดํ๊ฒ ๊ณต๊ฒฉ์๊ฐ ์๋ํ ํ์(์์ , ์ญ์ , ๋ฑ๋ก ๋ฑ)๋ฅผ ํน์ ์น์ฌ์ดํธ์ ์์ฒญํ๊ฒ ํ๋ ๊ณต๊ฒฉ์ ๋งํ๋ค. ์ฌ์ดํธ ๊ฐ ์คํฌ๋ฆฝํ (XSS)์ ์ด์ฉํ ๊ณต๊ฒฉ์ด ์ฌ์ฉ์๊ฐ ํน์ ์น์ฌ์ดํธ๋ฅผ ์ ์ฉํ๋ ์ ์ ๋ ธ๋ฆฐ ๊ฒ์ด๋ผ๋ฉด, CSRF๋ ํน์ ์น์ฌ์ดํธ๊ฐ ์ฌ์ฉ์์ ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ์ ์ฉํ๋ ์ํ๋ฅผ ๋ ธ๋ฆฐ ๊ฒ์ด๋ค. ์ผ๋จ ์ฌ์ฉ์๊ฐ ์น์ฌ์ดํธ์ ๋ก๊ทธ์ธํ ์ํ์์ ์ฌ์ดํธ๊ฐ ์์ฒญ ์์กฐ ๊ณต๊ฒฉ ์ฝ๋๊ฐ ์ฝ์ ๋ ํ์ด์ง๋ฅผ ์ด๋ฉด, ๊ณต๊ฒฉ ๋์์ด ๋๋ ์น์ฌ์ดํธ๋ ์์กฐ๋ ๊ณต๊ฒฉ ๋ช ๋ น์ด ๋ฏฟ์ ์ ์๋ ์ฌ์ฉ์๋ก๋ถํฐ ๋ฐ์ก๋ ๊ฒ์ผ๋ก ํ๋จํ๊ฒ ๋์ด ๊ณต๊ฒฉ์ ๋ ธ์ถ๋๋ค.
์ด์ ๊ฐ์ CSRF ๊ณต๊ฒฉ์ ๋ง๊ธฐ ์ํ ์๋จ์ผ๋ก token์ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๋ฅผ ์ธ์ฆ์ ํ๋ ๋ฐฉ์์ด ์๋ค.
Django์์๋ form
ํ๊ทธ ์์ ์ถ๊ฐํ์ฌ ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
ํ์ง๋ง ์ด token์ ์ถ๊ฐํ๋ฉด์ ํ
์คํธํ๊ธฐ๊ฐ ๋ฒ๊ฑฐ๋ก์์ก๋ค.
์๋ต์ ๋ฐ์ html์๋ hidden
type์ input
์ผ๋ก token์ด ์์ฑ๋์ด์์ง๋ง, template html์๋ ์์ฑ๋์ง ์์๊ธฐ ๋๋ฌธ์ด๋ค.
FAIL: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yugyeong/Study/tdd-4-clean-code/superlists/lists/tests.py", line 19, in test_home_page_returns_correct_html
self.assertEqual(response.content.decode(), expected_html)
AssertionError: '<htm[229 chars] <input type="hidden" name="csrfmiddleware[206 chars]l>\n' != '<htm[229 chars] \n </form>\n <table id="id_[85 chars]l>\n'
์ด ๋ฌธ์ ๋ฅผ ๋ง๋๊ธฐ ์ ๊น์ง์ ํ ์คํธ๋ ๋งค์ฐ ์์กฐ๋ก์ ๋ค. ๋น๊ตํ๋ ๋์์ ๊ฐ์ ๋ชจ๋ ์๊ณ ์์๊ธฐ ๋๋ฌธ์ด๋ค.
์ธ์ ๊ฐ ๋๋คํ ๊ฐ์ ์ด๋ป๊ฒ ํ ์คํธํ์ง? ๋ผ๋ ์๋ฌธ์ด ๋ ์ ์ด ์์๋ค. ์ด ์ฝ๋๋ ์ข ํธ์ค๋น ๊ฐ ์ฐธ๊ณ ํ๋ผ๊ณ ๋ณด๋ด์ฃผ์ ๋งํฌ ์ ํ ์คํธํ๊ธฐ ์ด๋ ค์ด ์ฝ๋์ ํฌํจ๋๋ค๊ณ ์๊ฐํ๋ค.
๋ด๊ฐ ์๊ฐํ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ด 3๊ฐ์ง์๋ค.
1๋ฒ์ ์ฐธ ๋ฌด์ํ ๋ฐฉ๋ฒ์ด๋ค. ํ ์คํธ์ฉ html ํ์ผ์ ๋ ํ๋ ๋ง๋ค๊ฒ๋๋ฉด, ์ด๋ฒ์ ์ฝ๊ฒ ๋์ด๊ฐ์ง๋ผ๋ ์์ผ๋ก ๋น์ฉ์ด ์ด๋ง์ด๋งํ๊ฒ ๋ค ๊ฒ์ด๋ค.
2๋ฒ์ ํ ์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋์ ์ด๋ ค์ด ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋ ๋ฐฉ๋ฒ(์ข ํธ์ค๋น ์ถ์ฒ ๋ฐฉ๋ฒ!)์ด๋ค. ํด๋ฆฐ ์ํคํ ์ฒ์ ํจ๊ป ์๊ฐํด๋ณด๋ฉด csrf token์ ์ฌ์ฉํ๋ ๋ถ๋ถ์ ์์ ๊ฐ์ฅ ๋ฐ๊นฅ์ชฝ์ด ๋๊ณ html์ ์ข ๋ ์์ชฝ ์์ ์์นํ๋ค. ํ ์คํธํ๊ธฐ ์ด๋ ค์ด ์ฝ๋๋ฅผ ๋ฐ๋ก ๋นผ์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ด๋ค. (๋ถ๋ฆฌ๋ ํ๊ณ ํ ์คํธ๋ ํ๋) ๊ฐ์ฅ ์ด์์ ์ธ ๋ฐฉ๋ฒ์ด์ง๋ง, ํ์ฌ ํ ์คํธํ๊ณ ์๋ ๋ถ๋ถ(html)์ ๋ถ๋ฆฌํ๋ค๊ณ ํด์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๊ฟ ์ ์๋ ๊ฒ์ด ์๋๋ผ๋ ํ๋จ์ด ์ฐ๋ค.
3๋ฒ์ ํ ์คํธ์์ ์ ์ธ์ํค๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ฐ์ฅ ์ฝ๊ณ ๋น ๋ฅด๊ฒ ํ ์ ์๋ ๋ฐฉ๋ฒ์ด๊ธฐ๋ ํ๋ค.
๋๋ ๊ณ ๋ฏผ ๋์ 3๋ฒ์ ํํ๋ค. csrf token์ ํญ์ ๋ณ๊ฒฝ๋๋ ๊ฐ์ด๊ธฐ ๋๋ฌธ์ ํ ์คํธ์์ ์ ์ธํ๋ค๊ณ ํด์ ์ค์ ์ฝ๋์ ์ํฅ์ด ์๋ ๊ฒ์ ์๋๋ฏ๋ก ๊ด์ฐฎ์ ๊ฒ์ด๋ผ ํ๋จํ๋ค.
๋ ๋์ ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ์ ๋ถ์ด ๊ณ์๋ค๋ฉด ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋น :)
์คํ ์ค๋ฒํ๋ก์ฐ์์ ๋์ ๊ฐ์ ์ด์๋ฅผ ๊ฒช๊ณ ์๋ ์ง๋ฌธ์ ๋ฐ๊ฒฌํ๋ค. ๊ทธ์ ๋ํ ๋ต ์ค์ ์ ๊ท์์ผ๋ก csrf token ๋ถ๋ถ์ ์ฐพ์๋ด์ด ์ญ์ ํ ํ ๋น๊ตํ๋ ์ฝ๋๋ฅผ ๋ณด์๊ณ ๋๋ ์ด๋ฅผ ์ ์ฉํ๊ธฐ๋ก ํ๋ค.
๋ค์์ test.py
์ HomePageTest
ํด๋์ค์ ์ ์ฒด ์ฝ๋์ด๋ค.
import re
class HomePageTest(TestCase):
@staticmethod
def remove_csrf(html_code):
csrf_regex = r'<input[^>]+csrfmiddlewaretoken[^>]+>'
return re.sub(csrf_regex, '', html_code)
def assertEqualExceptCSRF(self, html_code1, html_code2):
return self.assertEqual(
self.remove_csrf(html_code1),
self.remove_csrf(html_code2)
)
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
self.assertEqualExceptCSRF(
render_to_string('home.html', request=request),
response.content.decode()
)
def test_home_page_can_save_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = '์ ๊ท ์์
์์ดํ
'
response = home_page(request)
self.assertIn('์ ๊ท ์์
์์ดํ
', response.content.decode())
self.assertEqualExceptCSRF(
render_to_string('home.html', {'new_item_text' : '์ ๊ท ์์
์์ดํ
'}),
response.content.decode()
)
์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ๋ฌธ์ ์์ด ํ ์คํธ๊ฐ ํต๊ณผ๋ ๊ฒ์ด๋ค.
์์ง TDD ์ด์ง๋ผ ์ฑ ์ ๋ฐ๋ผ๊ฐ๋ ๊ฒ๋ ์กฐ๊ธ์ ๋ฒ ์ฐจ๋ค.. ํ ์ฅ ๋๊ธธ๋๋ง๋ค ์ฝ์ง ์ค์ด๊ธด ํ๋ฐ, ๊ทธ๋๋ ์ฌ๋ฐ๋น :D