장고로 기본적인 기능을 구현했으니 이제 눈에 보이지 않는 세세한 부분을 보완해주려고 한다. 장고는 보안 관련 문제를 csrf token 과 비밀번호 해싱을 통해 어느 정도 보안할 수 있다.
csrf 는 장고의 공식 문서에서 이렇게 나와있다. 사이트는 여기
The CSRF middleware and template tag provides easy-to-use protection against Cross Site Request Forgeries. This type of attack occurs when a malicious website contains a link, a form button or some JavaScript that is intended to perform some action on your website, using the credentials of a logged-in user who visits the malicious site in their browser. A related type of attack, ‘login CSRF’, where an attacking site tricks a user’s browser into logging into a site with someone else’s credentials, is also covered.
요약하면 웹사이트에 악의적인 js 코드를 넣어 정보를 유출한다던가, 다른 링크를 포함시켜 해킹을 시도한다던가 하는 공격을 방지하기 위해 웹사이트가 유효한지 계속해서 확인해주는 기능이다.
사용 방법은 간단하다. templates 의 .html 부분의 <form></form>
안에 {% csrf_token %}
을 넣어주기만 하면 된다. 그러면 장고가 알아서 토큰을 변경해 유효한 사이트인지 검사해준다.
form 태그를 쓸 때마다 csrf_token 을 써주는 것이 귀찮다면 XMLHttpRequest 를 보낼 때 X-CSRFToken header 를 커스텀해서 쓰는 방법도 있다. 대부분의 js 프레임워크는 모든 요청마다 헤더를 같이 보내기 때문에 내가 일일히 form 태그마다 csrf 를 추가하지 않아도 헤더를 보낼 때 csrf token 이 알아서 같이 갈 것이다.
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
공식 문서에서는 csrf token cookie 가 csrftoken 이란 이름으로 디폴트 설정이 되어있지만, 접근할 때에는 CSRF_COOKIE_NAME 설정으로 가서 접근하라고 되어있다.
기본적인 create 기능과 함께 비밀번호 해싱을 제공하는 함수이다.
많은 기능을 제공하는 파이썬의 대표적인 프레임워크 답게 장고는 기본적인 유저 모델 또한 제공하고 있다.
이 유저모델의 기본 속성으로 제공된다. usermodel의 전체 API 는 여기
공식문서에서는 이 중 password 는 보안을 위해 raw 를 저장하지 않는다고 나와있다.
Django does not store raw (clear text) passwords on the user model, but only a hash (see documentation of how passwords are managed for full details). Because of this, do not attempt to manipulate the password attribute of the user directly. This is why a helper function is used when creating a user.
장고의 password management 에 관해서는 여기
비밀번호만 따로 해싱해서 저장하고 싶다면 User.objects.creat() 전에 hashed = set_password('비밀번호')
혹은 make_password('비밀번호')
를 사용하여 따로 해싱을 해줘도 되지만, Usermodel.objects.create_user(username='아이디', password='비밀번호')
을 사용하면 장고에서 자동적으로 비밀번호 해싱을 해준다.
나도 별도로 비밀번호 해싱을 하지 않고 그냥 request form 을 통해 받은 비밀번호를 create_user() 을 통해 저장해줬고, 그 결과
잘 해싱되어 저장된 것을 볼 수 있다. 장고의 해싱방법이 궁금하다면 공식 문서를 찾아보면 된다. 해싱된 결과를 보면 알겠지만 sha256 을 iteration 해서 사용한다고 한다.
저번에 인스타그램 클론 코딩을 했을 땐 그냥 이미지 주소 복사를 하여 넣어줬는데, 이번에는 그걸 조금 업그레이드 하여 개인 기기에 저장된 이미지를 업로드할 수 있도록 만드려고 한다.
그걸 위해서는 먼저 UserModel 에 ImageField 를 추가해준 다음 migrate 를 해준다.
profile = models.ImageField(upload_to="images/", null=True, blank=True)
statics 의 image/ 에 이미지를 넣어주고 사용할 것이다.
그 다음 pip install Pillow
를 통해 장고에서 이미지를 업로드할 수 있게 해주는 Pillow 란 모듈을 설치해준다.
user/views.py 의 sign_up() 에 프로필 이미지를 받는 부분을 추가해준다.
try:
profile = request.FILES['images']
except:
profile = None
trey ~ except 문을 통해 이미지가 안 올라갔을 때는 그냥 None 이 전달되도록 했다.
html에서는 프로필 사진을 등록하는 란을 추가해주었다.
<div class="form-group mt-2 mb-2">
<label for="email">사진</label>
<input type="file" class="form-control" id="image" name="image">
</div>
input type 을 file 로 해서 내 기기에 있는 파일을 등록할 수 있도록 하였다.
이 뒤에 settings.py 에서 미디어를 가져오는 경로를 명시해줘야 한다.
settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
그리고 중요한 거
<form class="form-area" method="post" action="/signup/" enctype="multipart/form-data">
signup.html 에 enctype="multipart/form-data"
이걸 꼭 명시해줘야 한다.
실행결과
잘 실행된다!
장고에는 auth 에 유저가 인증된 사용자인지 검증해주는 기능이 몇 개 있다. is_authenticated() 와 login_required 등이 대표적인데, 이런 메소드를 사용하다 메소드가 어떻게 작동하는지 궁금해졌다.
signin() 을 구현할 때 편하게 썼던 기능이다. 공식 문서는 여기
세션이랑 같은 원리로 유저 정보를 장고 프레임워크 세션에 저장한다고 한다. 공식 문서에서는 authenticate 로 username 과 password 가 있는지 검사한 뒤 login() 을 써서 세션을 생성할 것을 권장하고 있다.
from django.contrib.auth import authenticate, login
def my_view(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
...
else:
# Return an 'invalid login' error message.
...
반대로 로그아웃을 하면 생성되었던 세션을 지워준다. 로그아웃은 별도의 처리 없이 그냥 logout() 메서드만 써주면 된다.
When you call logout(), the session data for the current request is completely cleaned out. All existing data is removed. This is to prevent another person from using the same Web browser to log in and have access to the previous user’s session data. If you want to put anything into the session that will be available to the user immediately after logging out, do that after calling django.contrib.auth.logout().
from django.contrib.auth import logout
def logout_view(request):
logout(request)
# Redirect to a success page.
앞서 view_todo 나 마이페이지를 구현할 때 로그인이 되었는지 안 되었는지 검사하기 위해 @login_required 로 감싸주었다. 말 그대로 이 페이지의 메서드를 실행하기 위해서는 로그인이 필수라는 의미이다.
원본 메서드는
login_required(redirect_field_name='next', login_url=None)
이다. next 파라미터를 전달하지 않으면 settings.LOGIN_URL 에 디폴트로 설정되어 있는 '/accounts/login' 으로 가게 된다. 만약 내가 따로 redirect 할 url 을 설정해주고 싶다면 login_url 파라미터로 값을 전달해주거나 settings.LOGIN_URL 을 내가 커스터마이즈한 url 로 바꿔주면 된다.
from django.contrib.auth.decorators import login_required
@login_required(login_url="/accounts/login/")
def my_view(request):
...
혹은
from django.contrib.auth import views as auth_views
path("accounts/login/", auth_views.LoginView.as_view()),
참고로 장고 공식 문서에서는 login_required 의 역할을 이렇게 설명하고 있다.
- If the user isn’t logged in, redirect to settings.LOGIN_URL, passing the current absolute path in the query string. Example: /accounts/login/?next=/polls/3/.
- If the user is logged in, execute the view normally. The view code is free to assume the user is logged in.