Notitle프로젝트 - 토큰 로그인 과정 세부구현

jkky98·2024년 2월 15일
0

Project

목록 보기
14/21

앞서 로그인 구현을 했는데 처음하는 것이다보니 기능구현에 초점만 맞추어서 허술한 부분이 많았다. 예를 들어 리프레시 토큰을 액세스처럼 썼다던지, 토큰을 로컬 스토리지에 저장하는 방식(보안에 취약) 이런 점들을 개선하고, 토큰의 유효기간을 고려해서 재로그인을 해야하는 등의 방식을 구현해볼까 한다.

JWT Token

jwt는 두 가지 토큰을 발급한다. 리프레시 토큰과 액세스 토큰이다. 액세스 토큰의 경우 클라이언트가 로그인시 백엔드에서 액세스 토큰을 클라이언트에게 전달한다. 액세스 토큰의 유효기간은 짧다. 보통 5분 정도인데, 이러한 이유는 만약 로그아웃하지 않아 로그인이 무기한 유지될 경우 누가 계정에 접속할 수 있는 위험에 취약해지기 때문이다.

액세스 토큰

이러한 액세스 토큰은 프론트엔드에서 받아 어디에 저장해야할까? 브라우저에서의 로컬스토리지, 세션스토리지, 쿠키 중 하나에 저장해야한다. 로컬 스토리지의 경우 클라이언트가 직접 지우지 않는 이상 브라우저에 계속 남아있다.(데이터의 영구성), 세션 스토리지의 경우 브라우저를 닫는다면 사라진다. 이 두가지 스토리지 방식은 XSS라는 보안공격에 취약하다는 단점이 존재한다.

쿠키는 만료기간이 있는 key-value 저장소이다. 이곳에 저장하는 것은 XSS에 대해서는 걱정이 사라지지만, CSRF공격에 취약해진다. 하지만 CSRF는 방어 관점에서 XSS보다 괜찮고, 쿠키에 저장된 토큰 값을 직접적으로 가져오지 않기 때문에 쿠키에 저장하는 것이 세 가지 관점중에선 가장 합리적으로 보인다.

설계

리프레시 토큰 DB 저장 및 인덱스 전달

로그인 요청이 들어오면, 리프레시 토큰을 DB에 전달하고 리프레시 토큰으로 액세스 토큰을 만들어 리프레시 토큰이 저장된 테이블의 인덱스와 액세스 토큰을 프론트로 전달하는 VIEW를 만들어보도록 한다.

# models.py

class ModelRefreshToken(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    refresh_token = models.CharField(max_length=1000)

리프레시 토큰을 저장할 테이블을 하나 만들어주도록 한다. user 변수 부분은 들어온 아이디로 하여금 기존의 auth_user Table의 key를 활용한다. refresh_token은 아이디 비밀번호가 일치할 경우 refresh_token을 생성토록 한다.

class LoginView(APIView):
    def post(self, request):
        # 사용자 입력값 가져오기
        username = request.data.get('username')
        password = request.data.get('password')
        email = request.data.get('email', '')


        # 사용자 인증
        user = authenticate(username=username, email=email, password=password)

        if user is not None:
            # 사용자 인증 성공시 리프레시 토큰 생성

            # rest_framework_simplejwt에서 제공하는 RefreshToken 클래스를 사용하여 리프레시 토큰 생성
            refresh = RefreshToken.for_user(user)

            try:
                # 이미 존재하는 레코드를 get
                entry_refresh_token = ModelRefreshToken.objects.get(user=user)
            except ModelRefreshToken.DoesNotExist:
                # 존재하지 않는 경우에는 새로운 레코드를 생성
                entry_refresh_token = ModelRefreshToken.objects.create(
                    user=user,
                    refresh_token=str(refresh)
                )
            else:
                # 이미 존재하는 경우에는 해당 레코드의 필드를 업데이트
                entry_refresh_token.refresh_token = str(refresh)
                entry_refresh_token.save()

            # refresh_key로 해당하는 RefreshToken 인스턴스를 생성
            refresh_token_instance = ModelRefreshToken.objects.get(refresh_token=refresh)
            index_refresh = refresh_token_instance.user_id
            user_id = refresh_token_instance.user
            accessToken = refresh.access_token
            print(accessToken)
            return Response({'message': '로그인 성공',
                            'accessToken': str(accessToken),
                            'refreshIndex': str(index_refresh)})
        else:
            # 사용자 인증 실패
            return Response({'error': '유효하지 않은 사용자 정보입니다.'}, status=status.HTTP_401_UNAUTHORIZED)
  1. 클라이언트에게 ID, PW 받음.
  2. 인증진행
  3. 인증성공시 : 유저아이디를 매개변수로 받아서 리프레시 토큰 생성
  4. try문 - 이미 레코드가 존재한다면 해당 레코드를 이용하고 리프레시 정보만 업데이트, 그렇지 않다면 리프레시 정보를 담은 레코드 생성,
  5. 리프레시 Table에서 인덱스만 추출
  6. 리프레시 토큰으로 액세스 토큰 추출
  7. 리프레시 index 데이터와 액세스 토큰 문자열화 데이터 응답.

리프레시 토큰을 DB에 저장하는 이유
https://blogeon.tistory.com/entry/JWT%EC%9D%98-Refresh-Token%EA%B3%BC-Access-Token%EC%9D%80-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C
여기서 참고함. 보안관련해서 이게 좋다고 한다. 읽어보면 이해될 듯.

이제 로컬스토리지에는 리프레시토큰에 대한 인덱스가 담겨있고, 쿠키에는 액세스토큰이 담겨있다. 우리는 이제 지속적으로 이 토큰을 활용해야한다. 내가 설계한 방식에서는, 많은 방식에 있어서 우리는 rest_framework와 통신하게 될 것이다. 우리는 어떠한 통신에 있어서도 토큰을 보내고 검사를 받는 과정을 추가하게 될 것이다. 그렇다면 우리는 모든 axios메서드 구성에 토큰을 가져오는 코드를 추가해야한다. 같은 작업의 반복. 이것을 피할 방법이 바로 인터셉터이다.

인터셉터

우리는 이제 axios에 관련한 인터셉터를 만들도록 한다. 우리는 어떠한 컴포넌트에서든 axios통신시에 header에 accessToken과 refreshIndex를 같이 보낼 예정이다. 이것을 자동화하기 위해 main.js에 인터셉터를 정의하도록 한다.

//main.js
// axios Intercepter 설정
const axiosInstance = axios.create({
  baseURL: "http://localhost:8000",
});

// Request Interceptor: 모든 요청에 대해 AccessToken와 refreshIndex을 추가
axiosInstance.interceptors.request.use(
  (config) => {
    const accessToken = getAccessTokenFromCookie(); // 쿠키에서 AccessToken을 get.
    const refreshIndex = getRefreshIndex(); // LocalStorage에서 refreshIndex를 get.
    if ((accessToken, refreshIndex)) {
      config.headers.Authorization = `Bearer ${accessToken}`;
      config.headers.refreshIndex = refreshIndex;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 쿠키에서 AccessToken을 가져오는 함수
function getAccessTokenFromCookie() {
  const name = "AccessToken"; // 쿠키 이름
  const cookieArray = document.cookie.split(";"); // 쿠키를 세미콜론을 기준으로 나누어 배열에 저장

  for (let i = 0; i < cookieArray.length; i++) {
    let cookie = cookieArray[i];
    while (cookie.charAt(0) === " ") {
      cookie = cookie.substring(1);
    }
    if (cookie.indexOf(name) === 0) {
      return cookie.substring(name.length, cookie.length); // access_token 값 반환
    }
  }
  return ""; // access_token을 찾지 못한 경우 빈 문자열 반환
}
function getRefreshIndex() {
  return localStorage.getItem("refreshIndex"); // LocalStorage에서 refreshIndex 값 반환
}

// Vue 인스턴스에 Axios를 추가하여 전역에서 사용할 수 있도록 합니다.
const app = createApp(App);

// axios를 전역으로 등록
app.config.globalProperties.$axios = axiosInstance;
profile
자바집사의 거북이 수련법

0개의 댓글