이전 포스트에서 GET과 POST를 할 수 있는 간단한 endpoint를 구현해 보면서 HTTP 요청, 응답이 백엔드 서버와 클라이언트 사이에서 어떻게 일어나는지 간단하게 테스트 해보았다. 이를 응용해서 Login과 Comment 부분을 구현해 본다.
처음에는 Login이나 Comment를 달고 보는 기능이나 같은 앱 상에 있기 때문에 django 프로젝트 내에 하나의 앱에서만 작성해도 큰 무리가 없을 거라 생각하고 하나의 앱 내에서 models.py
와 views.py
를 다음과 같이 작성했다.
# <app-name>/models.py
from django.db import models
# Create your models here.
class Account(models.Model):
username = models.CharField(max_length=50)
email = models.CharField(max_length=50)
password = models.CharField(max_length=300)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'accounts'
class Comment(models.Model):
comment_user = models.CharField(max_length=50)
comments = models.CharField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'comments'
일단 계정과 관련한 모델을 하나 구성해주고 댓글과 관련된 모델도 하나 구성해주었다.
계정 모델 클래스는 username(유저아이디), email, password 등을 담을 수 있고 댓글 모델 클래스는 comment_user(댓글을 다는 유저)와 comments(댓글 내용)등을 담는다고 가정한다.
# <app-name>/views.py
import json
from django.views import View
from django.http import JsonResponse
from .models import Account, Comment
class LoginView(View):
def post(self, request):
data = json.loads(request.body)
username = data.get('username', None)
email = data.get('email', None)
password = data.get('password', None)
if Account.objects.filter(username=username) or Account.objects.filter(email=email):
if Account.objects.filter(password=password):
return JsonResponse({'message':"Login SUCCESS"}, status=200)
return JsonResponse({'message':'Login Failed - wrong password'}, status=403)
return JsonResponse({'message':'Login Failed - wrong account(username or email)'}, status=403)
def get(self, request):
pass
class CommentView(View):
def post(self, request):
data = json.loads(request.body)
comment_user = data.get('comment_user', None)
comments = data.get('comments', None)
if comment_user == None:
return JsonResponse({'message':'Please input comment user name'}, status=403)
elif comments == None:
return JsonResponse({'message':'Please input comments'}, status=403)
Comment(
comment_user=comment_user,
comments=comments
).save()
return JsonResponse({'message':'Comment has saved successfully'}, status=200)
def get(self, request):
return JsonResponse({'comments':list(Comment.objects.values())}, status=200)
LoginView
에 있는 post(self, request)
함수 구현 로직 : HTTP 요청으로 받아온 data를 json의 형태로 load 한 뒤에 해당 데이터들이 DB테이블에 있는지를 검증한다.(if문 이하) filter 메소드를 이용하여 검증하게 구현했는데, filter메소드는 parameter조건에 해당하는 데이터가 있으면 그 데이터의 QuerySet instance를 return 하고 아니면 비어있는 QuerySet을 return 한다. 하지만 위와 같은 로직에 치명적인 문제점이 있다. if Account.objects.filter(password=password):
return JsonResponse({'message':"Login SUCCESS"}, status=200)
이 부분은 결국 특정 유저의 비밀번호를 filter를 이용해 찾는 것이 아닌 전체 테이블에서 비밀번호에 해당하는 모든 data를 찾게 되는 것이므로 찾지 않을 유저의 정보도 비밀번호만 일치한다면 찾아 버리는 문제가 생겨 버린다. 따라서 일부 http request에 대해 요청을 받아 올 수 있지만 일반화 할 수 없는 로직이므로 다시 구현해야 하고 아래와 같이 다시 구현했다.
# 위에 from django.http import 부분에 HttpResponse 추가
def post(self, request):
user_data = json.loads(request.body)
username = user_data.get('username', None)
email = user_data.get('email', None)
password = user_data.get('password', None)
if Account.objects.filter(username=username).exists():
account = Account.objects.get(username=username)
if account.password == password:
return JsonResponse({'message':'Login SUCCESS'}, status=200)
return HttpResponse(status=401)
elif Account.objects.filter(email=email).exists():
account = Account.objects.get(email=email)
if account.password == password:
return JsonResponse({'message':'Login SUCCESS'}, status=200)
return HttpResponse(status=401)
return HttpResponse(status=400)
Json의 형태로 username과 email을 둘다 받을지 둘중 하나만 받을지 그 상황을 고려하여 (실제 로그인 화면을 보면 여러개의 계정의 형태를 모두 입력가능하게 하였고 유효한 형태중 하나면 로그인이 가능한 계정으로 판단되어 비밀번호가 맞을 시 로그인이 되게 되어있다.
모든 입력형태를 고려하기 위해 각각의 받을 값들에 대해 있다면 얻어오고 없다면 None(없는 것을 명시)
의 형태로 json 데이터들을 받아왔다.
그래서 filter().exists()
메소드를 통해 해당 데이터가 있는지 검증한 후 해당 데이터에 맞게 DB테이블에서 얻어오고 그 다음에 비밀번호 검증이 되서 유효 하다면 Login SUCCESS 메세지를 보내게끔 하였다.
이외의 에러에 관해선 status상태만 유효하게 맞춰주고(401 : 권한 없음, 400 : 잘못된 형태의 요청 구문) HttpResponse
의 형태로 응답하게끔만 처리했다. 상세한 debug를 하지 않는 이상 보안적인 문제도 있고 유저에게 필요 이상으로 문제에 대한 원인을 제공할 필요가 없기 때문이다.
CommentView
도 각각 comment_user
와 comments
가 있을 경우에만 post가 되게끔 구성했다 에러 메세지도 간단하게 처리되는게 권장되지만 실제 서비스에서 에러 메세지를 내보내는 상황을 가정하여 만들었다. 사실 이부분은 Front-end 부분에서 구현 되어야 할 문제이기에 이렇게 까지 자세하게 에러 메세지를 서술할 필요는 없다.
작성한 app을 테스트 해 보면 다음과 같은 결과를 얻을 수 있다.
$ http -v http://127.0.0.1:8000/westargram username=abc password=1234
POST /westargram HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 39
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"password": "1234",
"username": "abc"
}
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: application/json
Date: Sat, 08 Feb 2020 07:11:40 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"message": "Login SUCCESS"
}
$ http -v http://127.0.0.1:8000/westargram username=abc passord=9999
POST /westargram HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 39
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"password": "9999",
"username": "abc"
}
HTTP/1.1 401 Unauthorized
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sat, 08 Feb 2020 07:21:35 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
$ http -v http://127.0.0.1:8000/westargram username=abc email=abc@def.com password=1234
POST /westargram HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 64
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"password": "1234",
"pemail": "abc@def.com",
"username": "abc"
}
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: application/json
Date: Sat, 08 Feb 2020 07:19:58 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"message": "Login SUCCESS"
}
$ http -v http://127.0.0.1:8000/westargram username=abc pemail=abc@def.com
POST /westargram HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 44
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"pemail": "abc@def.com",
"username": "abc"
}
HTTP/1.1 401 Unauthorized
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sat, 08 Feb 2020 07:20:21 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
$ http -v http://127.0.0.1:8000/westargram username=fff
POST /westargram HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 19
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"username": "fff"
}
HTTP/1.1 400 Bad Request
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sat, 08 Feb 2020 07:21:13 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
$ http -v http://127.0.0.1:8000/westargram/main comment_user=abcdef
POST /westargram/main HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 26
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"comment_user": "abcdef"
}
HTTP/1.1 403 Forbidden
Content-Length: 36
Content-Type: application/json
Date: Sat, 08 Feb 2020 07:00:35 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"message": "Please input comments"
}
http -v http://127.0.0.1:8000/westargram/main
GET /westargram/main HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
HTTP/1.1 200 OK
Content-Length: 594
Content-Type: application/json
Date: Sat, 08 Feb 2020 07:10:01 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"comments": [
{
"comment_user": "hihi",
"comments": "helloworld",
"created_at": "2020-02-07T07:23:57.457Z",
"id": 1,
"updated_at": "2020-02-07T07:23:57.457Z"
},
{
"comment_user": "hihi",
"comments": "helloworld",
"created_at": "2020-02-07T07:24:37.057Z",
"id": 2,
"updated_at": "2020-02-07T07:24:37.057Z"
},
{
"comment_user": "hihi",
"comments": "helloworld",
"created_at": "2020-02-07T07:24:42.431Z",
"id": 3,
"updated_at": "2020-02-07T07:24:42.431Z"
},
{
"comment_user": "hihi",
"comments": "helloworld",
"created_at": "2020-02-07T07:24:45.263Z",
"id": 4,
"updated_at": "2020-02-07T07:24:45.263Z"
}
]
}
기능적으로 무리없이 동작하는 것 같지만 여러가지 정보에 대한 보안처리나 다른 예외 상황들에 대해 테스트가 되지 않고 그저 데이터를 주고 받고 하는 최소한의 기능만 완성된 것이다. django project 내에서 app 설계시 권장되는 부분이나 실제 현업에서 쓰이는 구현 권장방식이 적용되지 않아서 또한 app의 구현 방향 및 목표가 명확하지 않아 무리하게 기능을 추가하려다 보니 엉뚱한 로직을 구현한 경우도 있었다. 따라서 다시 설계한 후 해당 부분에 대해 제대로 마무리 짓고자 한다.