[DRF] Custom User + JWT토큰 인증 방식 구현

 

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"
        }
     }
}