ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] JWT TOKEN(access token, refresh token, crfs, cors error)
    백엔드 2023. 5. 4. 12:14

    폴더 구조는 다음과 같다.

     


    다음과 같은 로직으로 코드를 짰다.

    회원가입

    #회원가입
    class SignupView(APIView):
        def post(self, request): #프론트에서 올린 데이터(request)
            serializer = SignUpSerializer(data=request.data)
            #입력된 데이터가 유효하다면,에러발생X
            if serializer.is_valid(raise_exception=False):
                user = serializer.save(request)
                response = Response(
                    {
                        "user_id": user.user_id,
                        "message": "회원가입 성공",
                    },
                    status=status.HTTP_200_OK,
                )
                return response
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    회원가입 api test

     

     

     

    로그인

    LoginSerializer에서 access token 과 refresh token을 발급해준 후,

    LoginView에서는 이미 발급된 토큰을 가져오기만 하는 방식으로 코드를 짰다.

     

    cf. 여기서 'authendicate' 대신 serializer을 쓴 이유는 "Django에서 authenticate 함수는 사용자 인증을 처리하고, 인증된 사용자 객체를 반환합니다. 그러나 authenticate 함수는 인증된 사용자 객체를 제공하지만, 이를 다른 시스템과 공유하거나 저장하려면 일부 작업을 수행해야합니다. 이를 위해 Serializer를 사용합니다." 라고 챗지피티씨가 말해줬기 때문이다.

    class LoginSerializer(serializers.ModelSerializer):
        user_id = serializers.CharField(
            required=True,
            write_only=True,
        )
    
        password = serializers.CharField(
            required=True,
            write_only=True,
            style={'input_type': 'password'}
        )
    
        class Meta:
            model = User
            fields = ['user_id', 'password']
    
    
        #아이디랑 비밀번호가 맞는지 확인
        def validate(self, data):
            user_id = data.get('user_id', None)
            password = data.get('password', None)
    
            if User.objects.filter(user_id=user_id).exists():
                user = User.objects.get(user_id=user_id)
    
                if not user.check_password(password):
                    raise serializers.ValidationError("wrong password")
            else:
                raise serializers.ValidationError("user account not exist")
    
            #유저가 존재하고, 아이디와 비밀번호가 일치한다면 RefreshToken.for_user를 이용해
            #user객체로부터 refresh token과 access token 생성
            token = RefreshToken.for_user(user)
            refresh_token = str(token)
            access_token = str(token.access_token)
    
            data = {
                'user': user,
                'access_token': access_token,
                'refresh_token': refresh_token,
            }
    
            return data

     

    위 코드에서 중요한 부분을 뽑으면 다음과 같다. 

    아이디와 비밀번호가 일치할 경우,

    rest_framework_simplejwt.tokens 라이브러리를 사용해 .for_user로 토큰을 발행해준다.

    from rest_framework_simplejwt.tokens import RefreshToken
    
    token = RefreshToken.for_user(user)
    refresh_token = str(token)
    access_token = str(token.access_token)

     

    serializer에서 발행해둔 토큰을 가져와 response의 cookie에 담아 프론트로 보내준다.

    #로그인
    #post요청을 받으면 LoginSerializer를 이용해 데이터를 검증하고, 유효한 데이터의 경우
    #유저 인증 후 access token을 LoginSerializer에서 가져와 response를 반환
    
    
    class LoginView(APIView):
        def post(self, request):
            serializer = LoginSerializer(data=request.data)
            if serializer.is_valid(raise_exception=False):
                #유효성 검사를 통과한 경우 토큰 확인
                #serializer.validated_data는 프론트에서 전송한 request.data에서 추출됨
                user_id=serializer.validated_data.get("user_id")
    
                # jwt token(refresh(장기), access(단기) 발급한걸 가져옴
                access_token = serializer.validated_data['access_token']
                refresh_token = serializer.validated_data['refresh_token']
    
    
                response = Response({
                    "user_id": user_id,
                    "message": "로그인 성공",
                    "token":{
                    "access_token": access_token.__str__(),
                    "refresh_token": refresh_token.__str__(),
                     }},
                    status=status.HTTP_200_OK, )
    
                #쿠키에 삽입 후 프론트로 전달
                response.set_cookie("access_token", access_token.__str__(), httponly=True, secure=True,
                                    max_age=60 * 60 * 1)  # 쿠키 만료 시간을 1시간으로 설정
                response.set_cookie("refresh_token", access_token.__str__(), httponly=True, secure=True,
                                    max_age=60 * 60 * 24)  # 쿠키 만료 시간을 24시간으로 설정
                return response
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    토큰 시간을 access_token을 더 짧게 적용해줌으로써 보안을 강화했다. 

    이 부분을 파일로 따로 뺄 수 있다고 들었는데, 구현하지는 못 했다.

     

    쿠키(Cookie)란?

    쿠키(Cookie)는 웹 브라우저에 의해 저장되는 작은 데이터 조각으로, 서버에서 브라우저로 전송되어 브라우저에 저장된다. 서버는 쿠키를 사용하여 사용자의 로그인 정보, 선호 설정, 장바구니 등을 저장하고 추적할 수 있다.

    하지만 쿠키는 다른 데이터와 함께 브라우저에 저장되기 때문에 보안 취약점이 될 수 있다.

    따라서 JWT 토큰을 쿠키에 저장할 때에는 보안 강화를 위해

    1. http-only 옵션: http-only 옵션은 JavaScript를 통한 쿠키 접근을 차단하여 XSS 공격을 방지한다.
    2. secure 옵션: Secure 옵션은 HTTPS 연결을 통해서만 쿠키를 전송하도록 제한하여 중간자 공격을 방지한다.
    3. 쿠키 유효 기간: 쿠키의 유효 기간을 설정하여 일정 시간이 지나면 만료되도록 설정할 수 있다. 적절한 만료 시간을 설정하여 보안을 강화할 수 있다.
    4. CSRF 보호: CSRF(Cross-Site Request Forgery) 공격을 방지하기 위해 적절한 CSRF 토큰을 사용하여 요청을 검증한다.

    를 사용해야한다.

     

     

    response.set_cookie("access_token", access_token.__str__(), httponly=True, secure=True, max_age=60 * 60 * 1)

     

     

    로그인 api test

     

     

     

    토큰을 통한 회원 정보 반환

    프론트에서 access token, refresh token을 통한 유저정보를 요구할 때 사용될 api이다.

    1. 프론트에서 axios get의 헤더에 'Bearer token'으로 유저정보 요구

    2. 백엔드에서 request에서 access token을 꺼내 일치하는 user_id를 찾음(decode)

    3. user_id에 해당하는 내용을 데이터베이스에서 가져옴

    4. 해당 내용을 response에 담아 프론트로 전송

     

    access token이 없다면?

    5. 프론트에서 access token 재발급을 위한 refesh token을 헤더에 담아 백엔드로 전송

    6. refresh token에서 찾은 user_id를 기반으로 새로운 access token 발행 후 프론트로 전송

     

    refresh token이 없다면?

    7. 사용자로 하여금 재로그인하도록 유도 => 로그인페이지로 리다이렉트

     

    class AuthView(APIView):
        def get(self, request):
            #access token을 프론트가 보낸 request에서 추출
            print(request)
            access_token = request.META['HTTP_AUTHORIZATION'].split()[1]
            print(access_token)
            #access token이 없다면 에러 발생
            if not access_token:
                return Response({"message": "access token 없음"}, status=status.HTTP_401_UNAUTHORIZED)
    
            #access token이 있다면
            #토큰 디코딩(유저 식별)
            try:
                #payload에서 user_id(고유한 식별자)를 추출
                #payload={'user_id:1'}
    
                payload = jwt.decode(access_token, SECRET_KEY, algorithms=['HS256']) #accesstoken 번호
                user_id = payload.get('user_id') #1로 넣었는데 5가 나옴
                print(user_id) #5
                #해당 유저 아이디를 가지는 객체 user을 가져와
                user = get_object_or_404(User, id=user_id) #id=5인 애를 가져와야 됨
                #UserSerializer로 JSON화 시켜준 뒤,
                serializer = UserSerializer(instance=user)
                #프론트로 200과 함께 재전송
                return Response(serializer.data, status=status.HTTP_200_OK)
    
             #Access token 예외 처리
            except jwt.exceptions.InvalidSignatureError:
                #access_token 유효하지 않음
                return Response({"message": "유효하지 않은 access token"}, status=status.HTTP_401_UNAUTHORIZED)
    
            except jwt.exceptions.ExpiredSignatureError:
                # access_token 만료 기간 다 됨
                refresh_token = request.COOKIES.get('refresh_token')
    
                #refresh_token이 없다면 에러 발생
                if not refresh_token:
                    return Response({"message": "refresh token 없음"}, status=status.HTTP_401_UNAUTHORIZED)
    
                try:
                    #refresh_token 디코딩
                    payload = jwt.decode(refresh_token, REFRESH_TOKEN_SECRET_KEY, algorithms=['HS256'])
                    user_id = payload.get('user_id')
                    user = get_object_or_404(id=user_id)
    
                    #새로운 access_token 발급
                    access_token = jwt.encode({"user_id": user.pk}, SECRET_KEY, algorithm='HS256')
    
                    #access_token을 쿠키에 저장하여 프론트로 전송
                    response = Response(UserSerializer(instance=user).data, status=status.HTTP_200_OK)
                    response.set_cookie(key='access_token', value=access_token, httponly=True, samesite='None', secure=True)
    
                    return response
    
                # refresh_token 예외 처리
                except jwt.exceptions.InvalidSignatureError:
                    # refresh_token 유효하지 않음
                    return Response({"message": "유효하지 않은 refresh token"}, status=status.HTTP_401_UNAUTHORIZED)
    
                except jwt.exceptions.ExpiredSignatureError:
                    # refresh_token 만료 기간 다 됨 => 이경우에는, 사용자가 로그아웃 후 재로그인하도록 유인 => 리다이렉트
                    return Response({"message": "refresh token 기간 만료"}, status=status.HTTP_401_UNAUTHORIZED)

    회원정보반환 api test

     

    아래는 프론트에서 백엔드로 보낼 코드이다.

    axios.get('/api/user', {
      headers: {
        'Authorization': `Bearer ${cookie}`
      }
    })

    cors 에러

    1. django-cors-headers 패키지 설치

    pip install django-cors-headers

    2. INSTALLED_APPS에 corsheaders 추가

    # settings.py
    
    INSTALLED_APPS = [
        # ...
        'corsheaders',
        # ...
    ]

    3. Middleware 추가

    # settings.py
    
    MIDDLEWARE = [
        # ...
        'corsheaders.middleware.CorsMiddleware',
        'django.middleware.common.CommonMiddleware',
        # ...
    ]

    4. 모든 도메인에 대해 허용

    # settings.py
    
    CORS_ORIGIN_ALLOW_ALL = True

    5. 부분 도메인에 대해 허용

    # CORS_ORIGIN_WHITELIST = [
    #     'localhost:3000',
    #     '127.0.0.1:3000'
    # ]

    => 에러남 해결못함

     


    과제 에러 해결

     

Designed by Tistory.