DRF에서는 미리 생성 된 user 모델을 제공하는데,
토큰 인증 방식을 추가로 구현하기 위해서 custom user를 구현했다.
DRF의 토큰 인증 방식
- 세션 인증과 토큰 인증 지원
- 세션 인증은 웹에 지원하고, 토큰 인증은 앱에 지원
- 기본 토큰 인증 방식은 랜덤 문자열 (유효기간 없음)
- JWT 토큰 인증 방식은 유효기간이 있으며 Refresh 토큰을 사용한 재인증 지원
-> 단순한 랜덤 문자열을 사용하는 기본 토큰 인증 방식 대신에 JWT 방식을 많이 사용함
-> 어플리케이션 개발(IOS, Android 등 )은 JWT 토큰 방식을 주로 사용
JWT 토큰 인증 방식
- DB조회 없이 로직만으로 인증
- 헤더에 내용을 담아서 전송하며 발급 시간을 저장
- access 토큰의 유효기간이 지나면 refresh로 재인증을하고,
refhresh 토큰이 만료(기간이 상대적으로 김)되면 username/password로 재로그인
Django Rest Framework : Simple JWT
- djangorestframework-jwt 기반으로 사용
- djangorestframework-simplejwt로 쉽게 사용 가능
설치 및 세팅
simple - jwt 설치하고 세팅
pip install djangorestframework-simplejwt
app - settings.py 설정
# Application definition
THIRD_PARTIES = [
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'rest_framework.authtoken',
'dj_rest_auth',
]
INSTALLED_APPS = [
......
......
] + THIRD_PARTIES
settings.py에 2가지 옵션 추가
- Rest_framework : 인증 클래스와 허용 클래스 선언
각 토큰 인증을 진행할 때 사용, 다른 view에서 토큰 인증이 되었는지 확인할 때 사용
- SIMPLE_JWT
원하는 옵션 골라서 사용
원하는 옵션을 골라서 필요한 부분만 수정한다.
꼭 설정해야 하는 옵션으로는 토큰의 인증시간, 타입, 알고리즘이다.
1. 토큰 인증 시간 : ACCESS_TOKEN_LIFETIME, REFRESH_TOKEN_LIFETIME
access 토큰은 30분, refresh 토큰은 일주일로 설정했다.
30분마다 토큰 재인증을 진행하고, 일주일이 지나면 자동 로그아웃 되도록 설정했다.
2. 토큰 타입 : AUTH_HEADER_TYPE
헤더로 받을 토큰의 타입이다.
'token' 으로 설정했는데, 클라이언트에서 token "token 내용" 으로 전송이 오면 구분한다.
Bearer을 가장 많이 사용한다.
3. 알고리즘 : ALGORITHM
토큰을 생성할때 사용하는 해쉬 알고리즘이다.
해당 알고리즘 규칙으로 토큰을 생성한다.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES' : [
'rest_framework_simplejwt.authentication.JWTAuthentication'
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
] # login작업이외에 다른 views에서 토큰을 사용할 때 필요
}
# 추가적인 JWT_AUTH 설정 (필요한 부분만 수정 )
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': False,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('token',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
# 'USER_ID_FIELD': 'id',
# 'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': datetime.timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': datetime.timedelta(days=1),
}
유저 생성
UserManager.py
유저를 생성할 때 도와주는 클래스
기존 유저 값이 존재하는지 확인하고 값을 리턴한다.
from django.contrib.auth.models import BaseUserManager
# BaseUserManager : drf 내의 user를 생성할 때 사용하는 헬퍼 클래스
# create_user : user 생성
# create_superuser : 관리자 생성
# is_superuser, is_staff -> True로
class UserManager(BaseUserManager):
def create_user(self,userName,password=None,**extra_fields):
if userName is None:
raise TypeError("Users must have userName!")
if password is None:
raise TypeError("Users must have password!")
user = self.model(
userName = userName,
**extra_fields
) #pass는 생략 (감추기)
# 대신 따로 제공하는 pw 함수 사용
user.set_password(password)
user.save() #저장
return user #리턴
# admin user : superuser
def create_superuser(self,userName,password, **extra_fields):
if password is None:
raise TypeError("Superuser must have password!")
# create_user로 저장 후 superuser로 지정
user = self.create_user(userName,password,**extra_fields)
# superuser로 지정
user.is_superuser = True
user.is_staff = True
user.save()
return user
User.py 클래스
- 유저 모델
class User(AbstractBaseUser,PermissionsMixin):
userName = models.CharField(max_length=255, unique=True,db_column='userName')
is_active = BooleanField(default=True)
is_staff = BooleanField(default=False)
USERNAME_FIELD = "userName" # 로그인 시 id로 사용
objects = UserManager()
def __str__(self):
return self.userName
유저 등록과 사용자 로그인을 위한 Serializer도 선언한다.
로그인 시에는 사용자 인증을 진행해서, 인증이 허용 된 사용자의 값만 응답한다.
동시에 last_login 객체를 생성해서 마지막 로그인 시간을 기록한다.
# 등록 , 로그인, 유저 정보 serializer 생성
class RegstrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
max_length = 128,
min_length = 4,
write_only = True
)
class Meta:
model = User
fields =[
'userName',
'password',
]
def create(self, validated_data):
userName = validated_data['userName']
if userName =='superuser':
print("슈퍼 유저가 생성 되었습니다.",userName)
return User.objects.create_superuser(**validated_data)
else:
return User.objects.create_user(**validated_data)
# 사용자 로그인
# username과 password를 확인 후 응답 전송
class LoginSerializer(serializers.Serializer):
userName = serializers.CharField(max_length=255)
password = serializers.CharField(max_length=128, write_only = True)
last_login = serializers.CharField(max_length=255,read_only = True)
# 유효성 검사
def validate(self, data):
userName = data.get('userName',None)
password = data.get('password',None)
if userName is None:
raise serializers.ValidationError(
'An userName is required to log in!'
)
if password is None:
raise serializers.ValidationError(
'An password is required to log in!'
)
user = authenticate(userName=userName,password=password)
#user data가 없다면 id , pw 오류
if user is None:
raise serializers.ValidationError(
'An user with this userName and pw was not found'
)
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated'
)
last_login = timezone.now()
user.last_login = last_login
user.save(update_fields=['last_login'])
print("user auth",user.userName)
return {
'userName':user.userName,
'last_login':user.last_login
}
View
가장 중요한 사용자 뷰이다.
회원 가입은 누구나 할 수 있으므로 permission_classes로 모두 허용을 부여한다.
새로 들어온 데이터를 직렬화해서 저장한 후, 해당 데이터를 토큰 인증에 사용한다.
이 때 토큰을 부여받기 위해서 TokenObtainPairSerializer의 get_token() 함수를 사용한다.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
토큰을 간단하게 생성하는데 도움을 준다.
get_token() 함수는 object를 파라미터로 받는다.
유저를 전달해준 후 발급받은 토큰을 응답으로 전송한다.
사용자 로그인 뷰에서는 요청온 데이터를 통해서 user 정보를 직렬화한다.
이 후에 직렬화 된 데이터가 유효한지 확인한 후 토큰을 발급받는다.
성공적으로 토큰이 발급 되었다면 사용자 정보와 토큰을 함께 클라이언트로 전송해준다.
# 회원가입 view
class RegistrationAPIView(APIView):
# 누구나 허용 (회원가입)
permission_classes = (AllowAny,)
serializer_class = RegstrationSerializer
renderer_classes = (UserJsonRenderer,)
# 요청
def post(self,request):
user = request.data
serializer = self.serializer_class(data=user) #직렬화
serializer.is_valid(raise_exception=True) #유효성 확인 +예외처리
user = serializer.save() # 저장
## 토큰 인증에는 object -모델을 전송해주어야 함
token = TokenObtainPairSerializer.get_token(user)
refresh_token = str(token)
access_token = str(token.access_token)
# 성공 응답 반환
return Response(
{"user":serializer.data,
"token":{
"access":access_token,
"refresh":refresh_token
}
},status=status.HTTP_201_CREATED)
# 로그인 view
class LoginAPIView(APIView):
permission_classes = (AllowAny,)
renderer_classes = (UserJsonRenderer,)
serializer_class = LoginSerializer
def post(self, request):
# 요청 온 user를 serializer에 보내줌
user = User.objects.get(userName=request.data["userName"])
serializer = self.serializer_class(data=request.data) #직렬화
serializer.is_valid(raise_exception=True) #유효성 확인 +예외처리
if user is not None:
token = TokenObtainPairSerializer.get_token(user)
refresh_token = str(token)
access_token = str(token.access_token)
#유저 존재 시 성공 응답
return Response(
{"user":serializer.data,
"token":{
"access":access_token,
"refresh":refresh_token
}
},status=status.HTTP_200_OK)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
URL
from rest_framework_simplejwt.views import TokenRefreshView,TokenObtainPairView
토큰을 발급받기 위해서 두가지 뷰를 사용한다.
두 뷰 모두 Django에서 자체적으로 생성된 뷰이다.
사용자 토큰을 발급하는 TokenObtainPairView와 리플래쉬 토큰을 발급하는 TokenRefreshView를 사용한다.
URL에 접근하는 것만으로 간단하게 토큰을 발급받을 수 있다.
#토큰 재발급
path("auth/refresh/",TokenRefreshView.as_view()),
# 토큰 생성
path("auth/token/",TokenObtainPairView.as_view())
모든 준비를 끝냈으니 클라이언트에서 토큰 인증 방식을 사용해보면 된다.
로그인 URL을 통해서 LoginApiView로 토큰을 발급 받는 것을 시도해봤다.
log를 찍어보니 성공적으로 토큰을 전달 받은 것을 확인할 수 있었다.
I/okhttp.OkHttpClient:
{"user":
{"user":
{"userName": "user1", "last_login": "2023-03-31 06:54:29.458503+00:00"},
"token":
{"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjgwMjQ3NDY5LCJpYXQiOjE2ODAyNDU2NjksImp0aSI6IjhlOTM3ZDhlZDlmMTQ0YjNiMTdlOGE3NWNiZGJkYTdlIiwidXNlcl9pZCI6Mn0.ruULBhiBB9Za-nc69G-y4RHrc8Ps4FPxv2PgqeodeNc",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY4MDg1MDQ2OSwiaWF0IjoxNjgwMjQ1NjY5LCJqdGkiOiIwYzU5NzEzM2NjMjM0ZWFlYWZhNDEzY2FlN2JlNzk5MiIsInVzZXJfaWQiOjJ9.XUr-GL3qk594BGu_QBAHUDI-R7Xf3RmunnvuPM9eDoc"
}
}
}