June 08, 2020
이전 포스트에서는 Django에서 JWT인증을 위한 미들웨어 작성법을 알아보았다.
이번에는 middleware
를 사용하기 위한 인증 토큰을 발행하는 과정을 다루어보겠다.
기본적으로 /login
과 /signup
두 가지의 path
에서 토큰을 발행된다.
인증과 관련된 로직을 처리하기 위한 user
앱을 새로만들었다.
그 후 앞서 얘기했던 것과 같이 로그인과 회원가입에 필요한 두 URL
을 설정한다.
아직 구현하진 않았지만 호출할 view로 login_view
와 signup_view
를 적어주었다.
user
앱의 urls.py
from django.urls import path
from user.views import login_view, signup_view
app_name = "user"
urlpatterns = [
path("login", login_view, name="login_view"), path("signup", signup_view, name="signup_view"),]
프로젝트 폴더의 urls.py
를 열어 작성한 user
앱의 urls.py
를 추가해주었다.
urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("", include("todo.urls", namespace="todo")),
path("", include("user.urls", namespace="user")), path("admin/", admin.site.urls),
]
URL
의 prefix가 모두 ""
이기 때문에 각각 /login
, /signup
으로 연결된다.
상당히 많은 모듈이 추가되었다.
기본적으로 요청에 대한 반환과 예외처리, JWT인증 및 토큰 생성을 위한 모듈이다.
자세한 내용은 모듈을 사용하면서 설명을 하겠다.
from django.http import JsonResponse, HttpResponseNotAllowed
from http import HTTPStatus
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.validators import ASCIIUsernameValidator
from user.utils.jwt import encode_jwt
from datetime import datetime, timedelta
from json import loads
토큰 생성은 utils
에 만들었던 encode_jwt
함수를 통해서 진행한다.
토큰에 들어가는 데이터에 관해 설명하면 아래와 같다.
iat
: 토큰 발행일exp
: 토큰 만료일 (토큰 발행일 + 7일)aud
: 토큰 발급자 (토큰을 발급한 사용자)iss
: 토근 발행자 (토큰을 발행한 주체)def generate_access_token(username):
iat = datetime.now()
exp = iat + timedelta(days=7)
data = {
"iat": iat.timestamp(),
"exp": exp.timestamp(),
"aud": username,
"iss": "Redux Todo Web Backend",
}
return encode_jwt(data)
이렇게 발행된 JWT토큰을 로그인, 회원가입에 성공하면 반환해준다.
csrf
와 관련된 인증은 사용하지 않을 것이기 때문에
decorator
를 이용해 csrf
인증을 사용하지 않음을 명시해주었다.
기본적인 반환은 JsonResponse
로 이루어지며 초기의 반환 데이터는
비어있는 딕셔너리 {}
와 상태 코드 200 OK
다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
return JsonResponse(data, status=status)
try-except
구문을 이용해 잘못된 요청이 왔을 경우 예외처리를 추가한다.
ValueError
: 사용자의 입력값이 잘못되었을 경우User.DoesNotExist
: 존재하지 않는 사용자일 경우ValueError
나 User.DoesNotExist
예외가 발생할 경우 400 BAD REQUEST
와
data
에 error
라는 key
로 에러메세지를 담아 반환한다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
try: pass except (ValueError, User.DoesNotExist): data["error"] = "Invalid form. Please fill it out again." status = HTTPStatus.BAD_REQUEST
return JsonResponse(data, status=status)
login
기능은 POST
메서드만을 사용해 처리가능하도록 구현하였다.
간단하게 request
의 method
필드를 확인해 "POST"
가 아닌 경우에는
405 Method Not Allowed
인 HttpResponseNotAllowed
를 반환하도록 해주었다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
try:
if request.method == "POST": pass else: return HttpResponseNotAllowed(["POST"])
except (ValueError, User.DoesNotExist):
data["error"] = "Invalid form. Please fill it out again."
status = HTTPStatus.BAD_REQUEST
return JsonResponse(data, status=status)
request
의 body
값을 가져와 json
을 dict
형태로 변환해준다.
그 후 "user"
와 "password"
값을 가져와 하나라도 없으면 ValueError
를 발생시킨다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
try:
if request.method == "POST":
json_body = loads(request.body) username = json_body.get("user", None) password = json_body.get("password", None) if not username or not password: raise ValueError()
else:
return HttpResponseNotAllowed(["POST"])
except (ValueError, User.DoesNotExist):
data["error"] = "Invalid form. Please fill it out again."
status = HTTPStatus.BAD_REQUEST
return JsonResponse(data, status=status)
User
모델을 사용해 username
값을 갖는 사용자가 있는지 확인한다.
여기서 해당 사용자가 없을 경우 User.DoesNotExist
예외가 발생한다.
해당 username
을 갖는 사용자가 있을 경우 user
객체의 check_password
를 이용해
해당 사용자의 비밀번호가 유효한 비밀번호인지 판단하게 된다.
요청을 받은 비밀번호가 잘못된 비밀번호 일 경우 ValueError
예외가 발생한다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
try:
if request.method == "POST":
json_body = loads(request.body)
username = json_body.get("user", None)
password = json_body.get("password", None)
if not username or not password:
raise ValueError()
user = User.objects.get(username=username) if not user.check_password(password): raise ValueError()
else:
return HttpResponseNotAllowed(["POST"])
except (ValueError, User.DoesNotExist):
data["error"] = "Invalid form. Please fill it out again."
status = HTTPStatus.BAD_REQUEST
return JsonResponse(data, status=status)
5번의 과정까지 모두 정상적으로 통과했다면 유효한 사용자임을 확인한 것이다.
generate_access_token
함수에 username
을 전달해 토큰을 생성하였다.
그 후 JsonResponse
의 데이터로 보낼 data
딕셔너리에 access_token
이라는
key
으로 JWT 토큰을 넣어주고 사용자 이름을 user
라는 key
로 넣어준다.
@csrf_exempt
def login_view(request):
data = {}
status = HTTPStatus.OK
try:
if request.method == "POST":
json_body = loads(request.body)
username = json_body.get("user", None)
password = json_body.get("password", None)
if not username or not password:
raise ValueError()
user = User.objects.get(username=username)
if not user.check_password(password):
raise ValueError()
data["access_token"] = generate_access_token(username) data["user"] = username
else:
return HttpResponseNotAllowed(["POST"])
except (ValueError, User.DoesNotExist):
data["error"] = "Invalid form. Please fill it out again."
status = HTTPStatus.BAD_REQUEST
return JsonResponse(data, status=status)
Postman
을 이용해 작성한 View
가 잘 동작하는지 확인해보겠다.
테스트를 위해 username
은 login_test
로 password
는 1234
로 계정을 생성했다.
이제 Postman
을 이용해 요청을 보내보도록 하겠다.
위와 같이 로그인을 할 데이터를 body
에 담아보냈다.
access_token
과 user
데이터가 잘 반환되는 것을 확인할 수 있다.
존재하지 않는 사용자를 넣어 예외 처리가 잘 되는지 확인해보자.
존재하지 않는 사용자로 요청시 400 BAD REQUEST
가 반환되는 것을 볼 수 있다.
허용되지 않는 메서드를 사용해 요청을 보내보자.
PUT
로 요청을 보내니 405 METHOD NOT ALLOWED
가 반환되는 것을 볼 수 있다.
그렇게 길지 않은 코드로 JWT 토큰을 발행하는 로그인 기능을 구현하였다.
다음 포스트에서는 JWT토큰을 발행하는 회원가입을 구현해보도록 하겠다.
추가적으로는 발행한 토큰을 이용해 데이터를 반환하는 과정또한 다루어 보도록하겠다.
실제 작성된 코드는 여기에서 확인할 수 있다.
질문과 오타, 문제점 제보는 환영입니다.