Notitle프로젝트 - JWT완결(simpleJwt)

jkky98·2024년 2월 21일
0

Project

목록 보기
15/21

Base Axios 인스턴스 구현

로그인과 회원가입 이외의 모든 axios통신에는 로그인 정보가 존재해야 작동하게 만들기 위해, 특정한 목적의 Axios 인스턴스를 고정적으로 생산할 수 있는 자바스크립트 파일을 만들었다.

첫번째로는 헤더에 인증에 필요한 액세스 토큰과 리프레쉬 토큰의 DB 인덱스이다.

import store from "./store/index";

export function setInterceptors(instance) {
  // axios interceptors 설정
  instance.interceptors.request.use(
    function (config) {
      // 토큰을 헤더에 추가
      const accessToken = store.state.accessToken;
      config.headers["Authorization"] = `${accessToken}`;
      config.headers["refreshIndex"] = store.state.refreshIndex;
      return config;
    },
    function (error) {
      // 요청 에러 처리
      return Promise.reject(error);
    }
  );

  // 응답 받기 전 인터셉터
  instance.interceptors.response.use(
    function (response) {
      return response;
    },
    function (error) {
      return Promise.reject(error);
    }
  );

  // 인터셉터가 정의된 인스턴스 반환
  return instance;
}

헤더에 다음과 같이 토큰과 리프레시 인덱스를 포함시킨다. 쿠키로 저장할까 하다가 vuex store기능을 활용했고, 가장 중요한 정보인 리프레시 토큰에 대한 정보는 어차피 직접적으로 기록되지 않으니 vuex-LocalStorage 조합을 사용했다.

import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";

export default createStore({
  // 상태를 정의합니다.
  plugins: [createPersistedState()],

  state: {
    popStateSignin: false,
    userId: "로그인이 필요합니다.",
    accessToken: "",
    refreshIndex: "",
    isLoggedIn: false,
  },

위의 코드는 store의 index.js의 한 부분을 가져온 것이고 중요한 것은 vuex-persistedstate를 사용했다는 것이다. 이는 vuex의 단점 중 하나인 새로고침시에 vuex의 데이터들이 사라지는 현상을 방지해준다.(LocalStorage를 활용)

import Axios from "axios";
import { setInterceptors } from "./interceptor.js";
import store from "./store/index";
import router from "./router";

// Axios 인스턴스 생성
const axiosInstance = Axios.create({
  baseURL: "http://localhost:8000",
});

setInterceptors(axiosInstance);

// 토큰 인증 //
axiosInstance
  .post("http://localhost:8000/api/auth/")
  .then((response) => {
    const { message, new_token } = response.data;
    if (message === "token is valid") {
      console.log("Token is valid");
    } else if (message === "token is expired") {
      console.log("Token is expired");
      store.commit("setAccessToken", new_token);
    } else if (message === "refresh token is expired, need to login") {
      console.log("Refresh token is expired, need to login");
      router.push({ path: "/" });
    }
  })
  .catch((error) => {
    console.error("Error while token verification:", error);
  });

export default axiosInstance; // Axios 인스턴스

인스턴스를 생성하는 js파일은 위와 같으며, 인스턴스를 생성하고 인터셉터 함수에 넣어서 인스턴스의 기본 헤더설정을 추가한 뒤에 토큰인증을 진행한다. auth endpoint로 인증을 시도하며 DRF의 auth과정은 다음과 같다.

# views.py (auth.py를 임포트)
class Auth(APIView):
    def post(self, request):
        return auth(request)
# auth.py
import requests
from rest_framework.response import Response
from rest_framework import status
from .models import ModelRefreshToken

def verify_token(token):
    url = 'http://localhost:8000/api/token/verify/'
    response = requests.post(url, data={'Token': token})
    return response.status_code == 400
    
def get_refresh_token(index):
    try:
        # 리프레시 토큰 가져오기.
        refresh_token = ModelRefreshToken.objects.get(user_id=index)
        return refresh_token
    except:
        # 못 가져올 경우
        return ConnectionError
    
def re_generate_access_token(refresh_token):
    url = 'http://localhost:8000/api/token/refresh/'
    try:
        response = requests.post(url, data={'refresh': refresh_token})
        return response.json()['access']
    except:
        return ConnectionError

# Full Logic  
def auth(request):
    # 토큰 가져오기
    token = request.headers.get('Authorization')
    index = request.headers.get('refreshIndex')
    # 토큰 유효성 검사
    if verify_token(token):
        return Response({'message': 'token is valid', 'new_token': ""}, status=status.HTTP_200_OK)
    else: # 토큰 만료시
        refresh_token = get_refresh_token(index)
        # 리프레시 유효성 검사
        if verify_token(refresh_token):
            # 리프레시 유효: 리프레시로 새로운 토큰 생성
            new_token = re_generate_access_token(refresh_token)
            return Response({'message': 'token is expired', 'new_token': new_token}, status=status.HTTP_200_OK)
        else:
            # 리프레시 유효하지 않을 때
            return Response({'message': 'refresh token is expired, need to login', 'new_token': ""}, status=status.HTTP_401_UNAUTHORIZED)

auth.py는 simpleJWT에서 제공하는 두가지의 view를 이용한다. 한가지는 검증을 해주는 view이고, 다른 한 가지는 리프레시토큰으로 액세스토큰을 발급해주는 view이다. 기존 생성된 리프레시 토큰은 직접적으로 DB에 저장되어 있으므로 리프레시 인덱스를 받아서 DB에 조회를 해서 사용한다. DB에 조회한다는 개념 때문의 토큰의 장점이 사라지는 것이 아닌가 싶지만 보안을 위한 약간의 양보의 개념으로 설계했다.

로그아웃

로그아웃을 구현 이전에, 토큰 방식에서 굳이 필요없는 작업이라는 의견이 많았다. 어차피 액세스 토큰은 짧은 시간만 유효하기 때문이다. 하지만 그냥 구현했다. jwt의 블랙리스트 기법을 사용할까 했지만 중요한 기능이 아니기에 로그아웃 시도시 토큰 검증만 진행하고, 브라우저에서 로그인 정보를 지우고 로그인 화면으로 돌아가게끔 했다.

<template>
  <v-app-bar app color="#26293C">
    <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
    <v-toolbar-title>
      {{ $route.meta.dynamicTitle }}
    </v-toolbar-title>
    <v-spacer></v-spacer>
  </v-app-bar>
  <v-navigation-drawer v-model="drawer" app color="#26293C">
    <v-list-item>
      <v-list-item-title class="text-h4 white--text">
        Consulting
      </v-list-item-title>
      <v-list-item-subtitle class="white--text">
        For College Entrance
      </v-list-item-subtitle>
    </v-list-item>
    <v-card class="mx-auto" max-width="344" color="primary">
      <v-card-item>
        <div>
          <div class="text-h6 mb-1">회원정보</div>
          <div class="text-h6">ID : {{ userId }}</div>
        </div>
      </v-card-item>

      <v-card-actions>
        <v-btn @click="Logout"> Logout </v-btn>
      </v-card-actions>
    </v-card>
  </v-navigation-drawer>

  <v-divider></v-divider>
</template>

<script>
import { mapState } from "vuex";
import axiosInstance from "../setaxios.js";
export default {
  name: "NavigationBar",
  data: () => ({
    drawer: false,
  }),
  computed: {
    ...mapState(["userId"]),
  },
  methods: {
    Logout() {
      console.log("Logout try..");
      const axiosInstanceLogout = axiosInstance;
      console.log(axiosInstanceLogout.defaults.headers);
      axiosInstanceLogout
        .post("http://localhost:8000/api/auth/", { userId: this.userId })
        .then((res) => {
          console.log(res.data);
          this.$swal
            .fire({
              title: `Good Bye ${this.userId}!`,
              confirmButtonText: "Success Logout!",
            })
            .then((result) => {
              /* Read more about isConfirmed, isDenied below */
              if (result.isConfirmed) {
                this.$swal.fire("Logout Success!", "", "success");
              }
            });
          // 기본 path로 이동
          this.$router.push("/");
          // 인증 정보 삭제
          this.$store.commit("setUserId", "");
          this.$store.commit("setAccessToken", "");
          this.$store.commit("setRefreshIndex", "");
          this.$store.commit("setIsLoggedIn", false);
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
};
</script>

화면단의 기능으로 추가된 것이 alert개념과 비슷한 VueSweetalert2를 사용하기로 했다. npm으로 설치한 후 main.js에 등록해 사용했다.(https://github.com/kingkingburger/Today_I_Learn/blob/master/vue/vue%EC%97%90%EC%84%9C%20alert%EC%B0%BD%20%EC%9D%B4%EC%81%98%EA%B2%8C%20%EB%B3%B4%EC%97%AC%EC%A3%BC%EA%B8%B0.md)
로그아웃은 네비게이션에 추가해서 사용하도록 한다.(왼쪽 끝에)

profile
자바집사의 거북이 수련법

0개의 댓글