프로젝트 기간 동안에는 pyJWT
를 배웠고, pyJWT
를 당연하게 사용해 왔다.
그러다가 기업 프로젝트를 하면서,
token을 주는 것에도 다양한 옵션이 있고 외부 lib/package를 사용하는 것에 대해 다시 한번 생각해볼 수 있는 계기가 되어, 정리해보기로 했다.
JWT 이전에는, 인증 작업은 session을 통해 이뤄졌다.
사용자의 인증 정보가 서버에 전달되면 서버는 session을 만들고,
클라이언트에 session id를 전달한다(클라이언트는 cookie에 이를 저장). session id가 만료되면 다시 처음부터 인증 - session id 생성 등을 진행한다.
django에서 처음 프로젝트를 생성하고 migrate을 하면, 아래와 같이 django_session
테이블이 생성되어 있음을 알 수 있다.
session을 생성하고 관련 정보를 server가 모두 관리함으로써 stateful한 상태가 되는데, 이 방법에는 몇 가지 단점이 있다
반면, 이러한 점 때문에 Netflix처럼 접속인원을 제한한다던가, 강제 로그아웃(세션 삭제) 등으로 활용할 수 있다.
어쨌든 위와 같은 이유로 최근에는 JWT를 대부분 사용한다.
(클라이언트에서 관리하므로 확장성에 문제가 없음)
pyJWT
라이브러리를 사용해 토큰 발행하는 방법이다.
pip install pyJWT
사용자가 로그인을 하면, 아이디/비밀번호 검증을 한 후, 토큰을 발행한다. JWT는 3가지 구성요소로 이뤄져 있는데 - header, payload, signature가 그것. 이렇게 구성된 타입을 Bearer 타입이라고 한다.
uuid
를 넣어 발행하기도 함) 실제 작성한 예시는 아래와 같다. (유효기간: 3일)
import pyJWT
from datetime import datetime, timedelta
access_token = jwt.encode({
'user_id':user.id ,
'exp':datetime.utcnow() + timedelta(days=3)},
settings.SECRET_KEY, settings.ALGORITHM
)
DRF 기반 JWT 생성 library로, JWT 기반이면서 DRF built-in token에 비해 보안이 좋아 많이 사용되고 있다.
(DRF built-in token은 상대적으로 문자열 구성이 단순하기 때문)
사용하려면 djangorestframework-simplejwt
를 설치해야 한다.
아직은 사용해본 적이 없어 우선 블로그를 통해 흐름을 이해했다.
blacklist
기능이 있으며, 사용자가 로그아웃하면 해당 token을 더이상 사용할 수 없도록 관리해준다.
블로그 - how to blacklist jwt in django
이번에 사용한 TokenAuthentication
은 공식 문서에 따르면, 아래와 같이 소개 되어 있다.
The
TokenAuthentication
class can be used to support REST framework's built-inTokenAuthentication
, as well as OAuth and JWT schemes.
DRF token에는 몇 개 옵션이 더 있다.
base-64
로 인코딩/디코딩해 인증하는 것으로, 실제 prod 환경에서 사용한다면 반드시 https
상에서 적용할 것을 권장한다 TokenAuthentication
기본 설정은 아래와 같다.
#settings.py
INSTALLED_APPS = [
#...
'rest_framework.authtoken',
]
...
...
RESTFRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
Serializer.py
는 아래와 같이 작성했다.
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(max_length = 128, write_only = True)
def validate(self, data):
email = data.get('email', None)
password = data.get('password', None)
user = authenticate(username = email, password = password)
if not user:
raise serializers.ValidationError('Invalid User')
return {'user' : user}
serializer
로부터 데이터를 받을 때는 request
가 아니라 data
authenticate()
는 username
과 password
등을 인자로 받아 동일한 사용자인지 아닌지 확인해 준다. 나는 email
을 username
으로 설정했으므로, username=email
이라고 작성.user
를 반환한다. 해당 user
객체를 반환해야, token 발행이 가능하다.user
는 결국 User.objects.get(id=user.id)
이다) None
을 반환한다.다음으로, views.py
를 작성한다.
#views.py
from rest_framework.authtoken.views import ObtainAuthToken
class LogInView(ObtainAuthToken):
permission_classes = [AllowAny]
serializer_class = LoginSerializer
def post(self, request):
serializer = self.seriazlier_class(data = request.data)
serializer.is_valid(raise_exception = True)
token, is_created = Token.objects.get_or_create(user_id = serializer.validated_data['id'])
return Response({'token' : token.key})
AllowAny
로 permission 설정 POST
로 전달되면, serializer
을 통해 검증 과정까지 거친다 (is_valid
) get
하고, 그렇지 않으면 create
해서 클라이언트에 전달한다그런데, 이렇게 쓰면 POST
를 2번 작성한 것이 된다.
왜냐하면 LoginView
는 ObtainAuthToken
을 상속받았고, 여기에는 위에 작성한 내용이 이미 모두 포함되어 있다.
따라서, 아래와 같이 작성하면 끝.
#views.py
from rest_framework.authtoken.views import ObtainAuthToken
class LogInView(ObtainAuthToken):
permission_classes = [AllowAny]
serializer_class = LoginSerializer
1 authenticate()
class ModelBackend(BaseBackend):
"""
Authenticates against settings.AUTH_USER_MODEL.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
2 token
class Token(models.Model):
"""
The default authorization token model.
"""
key = models.CharField(_("Key"), max_length=40, primary_key=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name='auth_token',
on_delete=models.CASCADE, verbose_name=_("User")
)
created = models.DateTimeField(_("Created"), auto_now_add=True)
class Meta:
abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super().save(*args, **kwargs)
@classmethod
def generate_key(cls):
return binascii.hexlify(os.urandom(20)).decode()
def __str__(self):
return self.key
3 ObtainAuthToken
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
if coreapi_schema.is_enabled():
schema = ManualSchema(
fields=[
coreapi.Field(
name="username",
required=True,
location='form',
schema=coreschema.String(
title="Username",
description="Valid username for authentication",
),
),
coreapi.Field(
name="password",
required=True,
location='form',
schema=coreschema.String(
title="Password",
description="Valid password for authentication",
),
),
],
encoding="application/json",
)
def get_serializer_context(self):
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
obtain_auth_token = ObtainAuthToken.as_view()
사실 프로젝트 진행하는 동안에는 이런 저런 외부 lib을 사용하는 것이 공부에 도움되기도 했기에 이에 대해 깊이 고민한 적은 없었다.
회사마다 다양한 컨벤션/규칙/개발 방향성이 존재하고,
현재 내가 프로젝트를 진행하고 있는 회사 또한 이러한 rule이 있다.
그 중 하나가 외부 lib 사용을 자제하는 것인데, 사실 이것 또한 생각해보면 유지/보수/확장과 연관되어 있는 문제였다는 것.
일단 첫 번째로는 용량의 문제다.
어쩌면 아주 일부만 사용하기 위해 그 lib을 통째로 갖고 와야하는 경우가 많은데, 그렇기엔 그 용량을 다 감당해야 해서 비효율적.
두 번째로는 추후 확장과 관련한 문제다.
외부 lib 사용을 남발하다 보면, 나중에 확장할 때 아주 어려워진다. build-up 이 복잡해지고 현재의 경우도 DRF에 이미 자체 token 기능이 있기 때문에 더더욱 외부 token 관련 lib을 갖고올 필요는 없는 것.
내가 처음simpleJWT
를 고려했던 이유는 보안이 DRF token보다 좋다고 느꼈기 때문인데, 그것뿐 아니라 위의 사항들도 고려해봐야 한다.
이러한 detail들도 회사 와서 많이 배우고 있다.
참고 자료
https://sherryhsu.medium.com/session-vs-token-based-authentication-11a6c5ac45e4
https://stackoverflow.com/questions/34013299/web-api-authentication-basic-vs-bearer
https://github.com/django/django/tree/271a8e73ee382bb487d15e97ffaa675d78869413
https://github.com/encode/django-rest-framework