document.addEventListener('DOMContentLoaded', function() {
    const profileuserId = window.location.pathname.split('/').slice(-2, -1)[0]; // 주소 URL에 있는 userId를 split을 통해 가져온다.

window.location.pathname.split('/').slice(-2, -1)[0]; :
window.location.pathname = URL이 https://whateversong.com/api/accounts/profile/userId/ 라면,
pathname은 /api/accounts/profile/userId/
.split('/') = '/'를 기준으로 나눈 배열, [' ', 'api', 'accounts', 'profile', 'userId', ' ']
.slice(-2, -1) = 배열을 뒤에서 두 번째부터 첫 번째까지 선택(종료 인덱스 선택 X), ['userId']
[0] = 최종 배열의 첫 번째 인덱스, 'userId'

 

function loadProfile() {
        const access = window.localStorage.getItem('access');  // 저장된 토큰 가져오기
        if (!access) {
            console.error('No access token found');
            return;
        }
        axios.get(`/api/accounts/api/profile/${profileuserId}/`,{
            headers: {
                'Authorization': `Bearer ${access}`  // 인증 토큰을 헤더에 추가
            }
        })
            .then(response => { // .then 메서드를 통해 비동기적으로 Promise를 반환
                const data = response.data;
                document.getElementById('username').textContent = data.username;
                document.getElementById('email').textContent = data.email;
                document.getElementById('nickname').textContent = data.nickname;
                if (data.image) {
                    document.getElementById('profile-picture').src = data.image;
                }
                loadEditProfileButton()
            })
            .catch(error => {
                console.error('Failed to load profile:', error);
            });
        };

.then 메서드

  1. 특징
    • Promise 기반의 비동기 작업에서 성공적으로 완료되었을 때 실행할 코드를 지정
  2. try catch와의 차이점
    • 비동기 처리 방식:
      • .then: Promise 기반의 비동기 처리 방법으로,
        비동기 작업이 완료된 후 실행할 콜백 함수를 지정
      • try...catch: async/await와 함께 사용되어 비동기 작업을 동기처럼 작성 가능,
        예외 처리가 간편
    • 코드 가독성:
      • .then: 중첩된 비동기 작업을 처리할 때 콜백 지옥(callback hell)을 일으킬 수 있음.
      • try...catch: async/await와 함께 사용하면 비동기 작업을 동기처럼 작성할 수 있어 가독성이 높아짐
    • 오류 처리:
      • .then: .catch 메서드를 사용하여 오류를 처리
      • try...catch: catch 블록을 사용하여 오류를 처리
 
    const menuLinks = document.querySelectorAll('.menu a'); // querySelectorAll은 html 요소에서 class 부분을 선택하는 메서드
    // 같은 이름의 모든 class를 다 선택한다.
    menuLinks.forEach(link => { // .forEach는 menuLinks에 포함된 모든 link들을 반복하여 아래 함수를 실행하는 메서드이다.
        link.addEventListener('click', function(event) {
            event.preventDefault();
            menuLinks.forEach(link => link.classList.remove('active')); // 모든 link에서 active 클래스를 제거
            this.classList.add('active'); // this는 이벤트가 발생한 요소를 뜻함, 여기선 클릭된 link 요소에 active 클래스를 추가한다는 의미
        });
    });
    document.getElementById('home-button').addEventListener('click', function() {
        window.location.href = '/api/accounts/api/main/';  // 메인 페이지로 이동
    });

querySelector

querySelector : 문서 내에서 제공된 선택자(셀렉터)와 일치하는 첫 번째 요소를 반환하는 메서드,
선택자는 CSS 선택자와 동일한 형식으로 작성

querySelectorAll : 문서 내에서 제공된 선택자와 일치하는 모든 요소를 반환하는 메서드,
반환된 요소들은 NodeList 객체에 담겨 있으며, 이는 배열처럼 반복 가능

forEach

배열이나 NodeList와 같은 반복 가능한 객체에 대해 각 요소를 반복하면서
제공된 콜백 함수를 실행하는 메서드

 

function displayPlaylist(playlists) {
    const container = document.getElementById('zzim-playlist-container');
    container.innerHTML = ''; // HTML 요소의 내용(content)을 설정하거나 가져오는 속성

    playlists.forEach(playlist => {
        const item = document.createElement('div');
        item.className = 'playlist-item'; // HTML 요소의 클래스 속성을 설정하거나 가져오는 속성

        // 이미지 URL이 존재하는지 확인하고 설정
        const imageUrl = playlist.image_url || 'https://via.placeholder.com/150';

        // 콘솔에 playlist 데이터 전체 출력
        console.log(`Playlist Data: ${JSON.stringify(playlist)}`);

        // playlist.id를 고유 식별자로 사용
        const playlistId = playlist.id;
        item.innerHTML = `
            <a href="${playlist.link}" target="_blank">
                <img src="${imageUrl}" alt="${playlist.name}">
                <div class="playlist-info">
                    <h2>${playlist.name}</h2>
                </div> 
            </a>
            <button class="zzim-button" data-id="${playlistId}">♡</button>
        `;
        container.appendChild(item); // 특정 부모 요소에 새로운 자식 요소를 추가하는 메서드
    });

.innerHTML

  • 설정할 때: 해당 요소의 모든 기존 내용이 새로운 HTML 문자열로 대체
  • 가져올 때: 해당 요소의 HTML 콘텐츠가 문자열로 반환
  • container.innerHTML = '';: zzim-playlist-container 요소의 기존 내용을 모두 지움
  • item.innerHTML: item 요소의 내부 HTML을 설정하여 플레이리스트 정보를 표시

.className

  • 설정할 때: 요소의 기존 클래스 목록이 지정한 클래스 문자열로 대체
  • 가져올 때: 요소의 클래스 목록이 문자열로 반환

.appendChild

  • 사용할 때: 부모 요소에 새로운 자식 요소를 추가
  • container.appendChild(item);: container 요소에 새로 생성한 item 요소를 자식으로 추가
 
function displayPosts(posts) {
    const postList = document.getElementById('post-container');
    postList.innerHTML = ''; 

    posts.forEach(post => {
        const postElement = document.createElement('div');
        let postImage = post.image ? post.image : window.mainLogoImage;
        postElement.classList.add('post');
        const truncatedContent = post.content.length > 50 
                ? post.content.substring(0, 50) + '...' 
                : post.content;
        postElement.innerHTML = `
            <a href=/api/posts/${post.id}/>
            <img src=${postImage}>
            <div class="content">
                <p id="post-title">${post.title}</p>
                <p id="post-content">${truncatedContent}</p>
                <div class="author-create-like">
                    <p>${post.category}</p>
                    <p>${formatDate(post.created_at).toLocaleString()}</p>
                    <p>좋아요 ${post.like_count}</p>
                </div>
            </div>
            </a>
        `;
        postList.appendChild(postElement);
    });
}

const trancatedContent = post.content.length > 50 ? post.content.substring(0, 50) + '...' : post.content;

? : 조건 ? 참일 때 반환 값 : 거짓일 떄 반환 값

.substring(0, 50) : 문자열의 특정 부분을 추출하는 메서드,
추출을 시작할 위치의 인덱스(0부터 시작),
추출을 종료할 위치의 인덱스 (생략할 경우 문자열의 끝까지 추출)

'JS' 카테고리의 다른 글

Whateversong base.js JS로직 기본 설명  (1) 2025.03.12
JWT Token을 이용한 로그인 기능  (0) 2025.03.12
function getCsrfToken() {
    return document.getElementById('csrf-token').value;

CSRF 토큰을 가져오는 전역 함수, HTML에 포함시킨 CSRF Token을 base.js에 가져와,
모든 html에서 필요할 때 함수를 불러와 사용할 수 있도록 함

document.getElementById('csrf-token').value; : html 태그 안에 **지정한 id 값**을 가져와
.value를 이용해 태그 안에 **value 값을 반환**하는 함수를 생성
--> 여기서 value값은 서버에서 생성해서 html에 저장한 csrf token을 의미

 <input type="hidden" id="csrf-token" value="{{ csrf_token }}">

 

CSRF(Cross-Site Request Forgery) Token

  1. CSRF 공격
    • 웹 애플리케이션의 취약점을 악용하여 사용자의 권한을 도용하는 공격 방식
    • 사용자의 의도와는 상관 없이 공격자가 원한 특정한 요청을 서버에 전송
  2. CSRF Token의 역할
    • CSRF 공격을 방지
    • 사용자가 폼을 제출할 때마다 함께 전송 → 서버는 요청이 정당한 사용자가
    • 의도적으로 보낸 것인지 확인
  3. 동작 방식
    • 토큰 생성
      • 사용자가 웹 애플리케이션에 접근할 때(페이지에 접속, 특정 페이지를 로드할 때) 서버에서 CSRF Token을 생성
      • 현재 우리 코드에서는 생성한 CSRF Token을 HTML에 저장 이는 구현이 쉽고, 클라이언트 쪽에서 상태를 유지할 필요가 없음
      • 다른 방식으로는 **사용자 고유 세션에 저장**하는 방식이 있음,서버가 세션 상태를 유지해야 하므로, 리소스를 더 많이 소모하고, 구현이 복잡함
      • 토큰이 클라이언트에 노출되지 않아 XSS 공격으로부터 비교적 안전하지만
    • 토큰 포함
      • 사용자가 폼을 제출하거나 데이터를 전송할 때, 토큰을 함께 전송
    • 토큰 검증
      • 서버는 요청이 들어올 때마다 전송된 토큰이 올바른지 확인
      • 요청에 토큰이 포함되지 않거나, 서버가 생성한 토큰과 일치하지 않는 경우
      • 403 Forbidden 에러 반환

function parseJwt(token) {
    try {
        return JSON.parse(atob(token.split('.')[1])); 
    } catch (e) {  
        return null;
    }
}

Access Token을 **디코딩**하는 함수,
Access Token은 Header, Payload, Signature 부분으로 .을 통해 3등분 되는데,
Token 정보를 표현하는 데 사용하는 Payload 부분을 추출

try, catch(e) : 올바른 인자를 갖추거나, 조건을 충족한 경우 try 안에 로직을 실행한다.
반대의 상황에서는 catch로 넘어가는데 ** catch (e) 혹은 catch (error)**로 표현하며,
오류가 났을 때 오류를 어떻게 처리할 지에 대한 로직을 작성한다.

token.split('.')[1] : Access Token을 점(.)으로 구분하여 2번째 부분(Payload) 추출

atob : Token의 Header와 Payload는 ** Base64URL**로 인코딩되어 있는데,
atob 은 이를 디코딩하여 원래의 문자열(JSON)로 변환하는 함수

JSON.parse : 디코딩된 JSON 문자열을 **JavaScript 객체**로 변환하는 함수
JavaScript 객체로 변환해야 JS 로직에서 쉽게 다룰 수 있음


JWT(JSON Web Token)

JWT는 JSON 객체를 안전하게 전송하기 위한 토큰 형식.
JWT는 점(.)으로 구분된 세 부분으로 구성:

  1. Header: 토큰 유형과 서명 알고리즘 정보를 포함
  2. Payload: 클레임(claim)이라고 하는 **사용자 정보나 기타 데이터**를 포함
  3. Signature: 토큰의 무결성을 검증하기 위한 서명
// Access token을 새로고침하는 함수
async function refreshAccessToken() { // refreshAccessToken 함수를 async 인자로 주어 비동기로 처리
    const refreshToken = localStorage.getItem('refresh'); // localStorage에 있는 refresh token을 refreshtoken이라는 상수로 선언 / const = 상수, let = 변수
    try {
        const response = await axios.post('/api/accounts/api/token/refresh/', { refresh: refreshToken }); // axios.post를 보내 response를 받아온다.
        const newAccessToken = response.data.access; // newAccessToken에 response로 받아온 새 Access Token을 담는다.
        localStorage.setItem('access', newAccessToken); // setItem은 생성 즉, newAccessToken을 'access'라는 이름으로 localStorage에 담는다.
        return newAccessToken;
    } catch (error) {
        console.error('Error refreshing token:', error);
        // localStorage에 담겨있던 모든 정보들을 초기화 한 후, 재로그인
        localStorage.removeItem('access');
        localStorage.removeItem('refresh');
        localStorage.removeItem('user_id');
        localStorage.removeItem('user_nickname');
        if (window.location.pathname !== '/api/accounts/login/') { // 로그인 페이지에 있지 않다면
            window.location.href = '/api/accounts/login/';
        }
        throw error; // error가 떴을 때, 이 함수를 실행하는 상위 함수를 실행 --> axios.interceptores.request.use가 여기서 해당
    }
}

async function refreshAccessToken() : Access Token을 Refresh Token을 이용하여 재발급 받는 함수, async 를 붙여서 이 함수가 **비동기 함수**가 되게 함 -> **Promise**로 반환

const response = await axios.post : await 뒤에 붙은 함수(여기선 axios.post 요청)가 완료될 때까지, async 에 붙은 함수는 대기, axios.post 에서 반환된 값을 response로 선언

axios.post('/api/accounts/api/token/refresh/', { refresh: refreshToken }) : axios 라이브러리를 이용해 post 요청을 **'refresh'**라는 이름으로 **refreshToken**을 담아 Access Token을 재발급 받는 **URL**에 보낸다.


Axios 라이브러리

  1. 역할
    • JavaScript에서 **HTTP 요청**을 보낼 때 사용하는 라이브러리
    • 특히, **비동기적**으로 서버와 통신할 수 있는 기능을 제공하여 웹 애플리케이션에서 널리 사용
    • 간결한 API와 다양한 기능을 통해 HTTP 요청을 쉽게 처리
  2. 특징
    • Promise 기반:
      • Axios는 **Promise API**를 사용, 이는 비동기 작업을 더 쉽게 관리하고,
        .then()  .catch() 구문을 통해 요청의 성공과 실패를 처리
    • 요청 및 응답 데이터 변환:
      • 요청을 보내기 전에 데이터를 자동으로 **JSON 문자열로 변환**하거나,
        서버로부터 응답받은 데이터를 자동으로 **JavaScript 객체**로 변환
    • 인터셉터(Interceptors):
      • 요청이나 응답을 가로채고, 이를 수정하거나 처리
        이를 통해 로깅, 권한 부여, 에러 처리 등을 중앙 집중식으로 관리
    • 자동화된 CSRF 보호:
      • CSRF(Cross-Site Request Forgery) 토큰을 자동으로 설정하고, 요청 **헤더**에 포함
    • 취소 토큰:
      • 요청을 **취소**할 수 있는 기능을 제공
        이는 사용자가 불필요한 요청을 중단하거나, 네트워크 상태에 따라 요청을 제어
  3. 설치
<!-- base.html -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

**CDN**을 통해 html에서 직접 로드 가능

// Axios 요청 인터셉터를 설정하여 자동으로 토큰을 갱신하는 함수
// interceptors란, 모든 HTTP '요청' 전에 호출해서 실행하는 함수 / 모든 '응답' 전에 호출하는 interceptors.response.use도 있다.
axios.interceptors.request.use(
    async config => { // config는 axios 요청의 설정을 포함하는 객체 (url, headers, method, params, data)
        // 특정 URL을 제외
        // interceptor에서 제외시킨 URL 중에, <int:pk>를 받는 URL들을 정규표현식으로 표현 --> 정규표현식으로 하지 않았을 때, 경로가 명확하지 않아 오류 발생
        const excludedUrls = [
        '/api/accounts/main/',
        '/api/accounts/signup/', 
        '/api/accounts/api/token/', 
        '/api/accounts/login/', 
        '/api/accounts/logout/', 
        '/api/accounts/api/signup/', 
        '/api/playlist/list/', 
        '/api/playlist/data/', 
        '/api/playlist/search/', 
        '/api/playlist/zzim/\\d+',
        '/api/posts/api/list/',
        '/api/posts/list/', 
        '/api/posts/api/\\d+', 
        '/api/posts/\\d+'];
        if (isExcludedUrl(config.url, excludedUrls)) {
            return config; // 제외한 URL을 넣었을 때 true가 나오면, interceptor를 그대로 종료
        }


        let accessToken = localStorage.getItem('access');  // let으로 선언하여 재할당 가능
        const tokenData = parseJwt(accessToken);
        const now = Math.ceil(Date.now() / 1000); // Math.ceil -> JS 내장 함수로 반올림하는 역할, Date.now는 현재 시간을 밀리초로 나타내고, 
        // 밀리초를 1000으로 나누어 초 단위로 변경

        // 토큰이 만료되었는지 확인
        if (tokenData.exp < now) {
            accessToken = await refreshAccessToken();  // 만료된 경우 새 토큰 발급
        }

        config.headers['Authorization'] = 'Bearer ' + accessToken; // config headers 부분 Authorization에 Access Token을 추가
        return config;
    },
    error => {
        return Promise.reject(error); // 'error'를 이유로 즉시 실패하는 'Promise'객체를 생성
    } // async 함수는 명시하지 않아도 항상 Promise 객체를 반환, interceptor에서 오류가 발생할 시, Promise.reject로 반환하여 error를 확인할 수 있도록 함
); // catch가 있다면 async에서 catch로 error 처리를 하기 용이하게 하는 역할이지만, 여기서는 error 자체를 반환하도록 함
// 주어진 URL이 특정 패턴 목록 중 하나와 일치하는지 확인 --> 특정 패턴 = interceptor에서 excludedUrls로 선언한 URL
function isExcludedUrl(url, patterns) {
    return patterns.some(pattern => { // .some은 패턴이 URL과 일치하는지 검사 --> config.url = urls.py에 적힌 모든 url / 일치 시 true
        const regex = new RegExp(pattern.replace(/<int:\w+>/g, '\\d+')); // 모든 <int:...>를 \\d+(숫자 하나 이상)으로 변경
        return regex.test(url); // 변경된 URL이 정규 표현식과 일치하는 지 검사 일치 시 true
    });
}
function checkLoginStatus() { // login이 되 있는 지 확인하는 함수
    const accessToken = localStorage.getItem('access');
    const loginLogoutLink = document.getElementById('login-logout-link');
    const signupProfileLink = document.getElementById('signup-profile-link');
    if (accessToken) {
        loginLogoutLink.textContent = 'Logout';
        loginLogoutLink.style.cursor = 'pointer';
        loginLogoutLink.addEventListener('click', function(e) { // JS에서 addEventListener는 동작(여기서는 'click'을 의미 또다른 예로는 'submit')을 했을 때,
            e.preventDefault(); // e.preventDefault()를 통해 클릭 시 발생하는 기본 동작(폼 제출, 링크 이동 등)을 방지
            logout();
        });
        const userId = localStorage.getItem('user_id');
        signupProfileLink.textContent = 'Profile';
        signupProfileLink.href = `/api/accounts/profile/${userId}/`; // `` JQuery 문법을 이용하여 선언한 값을 문자열로 치환 python의 fstring과 같은 역할
    } else { 
        loginLogoutLink.textContent = 'Login';
        loginLogoutLink.href = '/api/accounts/login/';
        signupProfileLink.textContent = 'Signup';
        signupProfileLink.href = '/api/accounts/signup/';
    }
}

Document.addEventListener

  1. addEventListener는 eventType과 eventHandler를 인수로 받는다.
    • eventType - 'click', 'submit', 'load', 'keydown' 등
    • eventHandler - 이벤트가 발생했을 때 실행되는 함수
      • function(e) or function(event)
        eventHandler 함수는 일반적으로 이벤트 객체(e, event)를 인수로 받는다.
        이 이벤트 객체는 이벤트에 대한 다양한 정보를 포함하고 있다.
  2. e.preventDefault()
    • 이벤트의 기본 동작을 막는 것,
      위 함수를 예로 들면, 'click' 시 페이지가 새로고침 되는 동작을 막는다.

JQuery

  1. 정의
    • JavaScript를 더 쉽고 간편하게 사용할 수 있도록 도와주는 경량의 JavaScript 라이브러리
    • DOM(Document Object Model) 조작, 이벤트 처리, 애니메이션, AJAX 요청 등을
      단순화
  2. 사용 이유
    • `/api/accounts/profile/${userId}/` : URL을 JQuery로 작성함으로써,
      userId를 받아와 URL주소에 넣어주어 속성을 동적으로 설정
function logout() {
    const refreshToken = localStorage.getItem('refresh');
    const csrfToken = getCsrfToken();
    axios.post('/api/accounts/logout/', { refresh: refreshToken }, {
        headers: {
            'X-CSRFToken': csrfToken
        }
    })
    .then(response => {
        localStorage.removeItem('access');
        localStorage.removeItem('refresh');
        localStorage.removeItem('user_id');
        localStorage.removeItem('user_nickname');
        window.location.href = '/api/accounts/login/';
    })
    .catch(error => {
        console.error('로그아웃 실패.', error);
    });
}

X-CSRFToken

사용 이유

  1. 표준 및 관례: 많은 웹 프레임워크(예: Django, Flask)는 CSRF 토큰을 HTTP 헤더
    X-CSRFToken 필드에 담아 전송하도록 설계,
    이는 클라이언트와 서버 간의 일관된 보안 메커니즘을 유지하기 위함
  2. 명확한 목적: X- 접두어는 일반적으로 비표준 헤더를 나타내며,
    CSRF 토큰을 포함한 헤더는 HTTP 표준 헤더가 아니므로 X-를 사용하여 명확히 구분
  3. 보안 및 가독성: 헤더에 CSRF 토큰을 포함하면, URL 또는 요청 본문에 포함하는 것보다 더 안전,
    이는 토큰이 쉽게 노출되지 않도록 하여 보안을 강화하는 데 도움,
    또한, 헤더에 포함하면 요청의 의도를 명확하게 전달

'JS' 카테고리의 다른 글

Whateversong Profile.js JS 로직 설명  (0) 2025.03.12
JWT Token을 이용한 로그인 기능  (0) 2025.03.12

JWT Token을 선택한 이유

  1. 서버 간 세션을 공유해야 하는 세션 로그인보다 서버 부하 측면에서 이점을 챙길 수 있는 JWT Token이 사용하기에 더 적합하다고 생각하여 사용
  2. JSON 통신을 통해 JavaScript를 이용하여 클라이언트-서버 간의 상호작용을 원활하게 하기 위해 JWT Token 로그인을 사용
  3. JWT는 서명된 토큰을 사용하여 토큰의 무결성을 검증할 수 있으므로, 짧은 만료 시간과 리프레시 토큰을 사용하여 보안을 강화할 수 있다고 생각하여 사용

JWT Token 기본 로직 설명

// base.js
function parseJwt(token) {
    try {
        return JSON.parse(atob(token.split('.')[1])); 
    } catch (e) {
        return null;
    }
}

JWT 토큰을 디코딩하는 함수,
Access Token은 Header, Payload, Signature 부분으로 "."을 통해 3등분 되는데,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.(Header)
eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzE3NTEyNzA3LCJpYXQiOjE3MTc1MTA5MDcsImp0aSI6Ijg5OTUxNjJhZjY2NzRhOTg4Y2MxYjE3MjMyNzUxMjViIiwidXNlcl9pZCI6MSwidXNlcl9uaWNrbmFtZSI6IkpJTiJ9.(Payload)
-eJKR2QLJAzFW2zm9g8VLeaDjZLP1FdGwYv_amTi7e4(Signature)
Token 정보를 표현하는 데 사용하는 Payload 부분을 추출하기 위해,
Access Token을 디코딩하는 함수

// base.js
async function refreshAccessToken() { 
    const refreshToken = localStorage.getItem('refresh');
    try {
        const response = await axios.post('/api/accounts/api/token/refresh/', { refresh: refreshToken }); 
        localStorage.setItem('access', newAccessToken);
        return newAccessToken;
    } catch (error) {
        console.error('Error refreshing token:', error);
        localStorage.removeItem('access');
        localStorage.removeItem('refresh');
        localStorage.removeItem('user_id');
        localStorage.removeItem('user_nickname');
        if (window.location.pathname !== '/api/accounts/login/') {
            window.location.href = '/api/accounts/login/';
        }
        throw error; 
    }
}

refreshAccessToken() : Refresh Token을 이용하여 Access Token을 재발급 받는 로직

const refreshToken = localStorage.getItem('refresh'); : 로그인 시 localStorage에 저장한 Refresh Token을 refreshToken이라는 상수로 선언

try { const response = await axios.post('/api/accounts/api/token/refresh/', {refresh: refreshToken }); localStorage.setItem('access', newAccessToken); return newAccessToken;

# accounts/urls.py
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),

urls.py에서 지정해 준 TokenRefreshView로 연결하는 url에 axios.post 요청을 보낼 때,
Refresh Token을 담아서 보내 준 뒤, response로 받아온 Access Token을
newAccessToken으로 return해준다.

} catch (error) { console.error('Error refreshing token:', error);: error, 즉, Refresh Token이 만료되거나, 이미 Blacklist에 들어갔을 때 새로운 Access Token을 발급받지 못하고, error가 생기는데, 그 때는 로그인 시 생성했던 localStorage에 있는 모든 정보를 제거

if (window.location.pathname !== '/api/accounts/login/') { window.location.href = '/api/accounts/login/'; } throw error;} }: (이미 로그인 페이지인 것이 아니라면) 로그인 페이지로 이동
throw error는 catch 블록 내부에서 발생한 예외를 다시 던지는 역할,이로 인해 refreshAccessToken 함수를 호출한 상위 코드에서 이 예외를 캐치

// base.js
axios.interceptors.request.use(
    async config => { 

        const excludedUrls = ['/api/accounts/signup/', 
        '/api/accounts/api/token/', 
        '/api/accounts/login/', 
        '/api/accounts/logout/', 
        '/api/accounts/api/signup/', 
        '/api/playlist/', 
        '/api/playlist/data/', 
        '/api/playlist/search/', 
        '/api/playlist/zzim/<int:playlist_id>/',
        '/api/posts/',
        '/api/posts/list/', 
        '/api/posts/api/<int:post_id>', 
        '/api/posts/<int:post_id>'];
        if (excludedUrls.some(url => config.url.includes(url))) { 
            return config;
        }

        let accessToken = localStorage.getItem('access');  
        const tokenData = parseJwt(accessToken);
        const now = Math.ceil(Date.now() / 1000);



        if (tokenData.exp < now) { 
            accessToken = await refreshAccessToken();

        config.headers['Authorization'] = 'Bearer ' + accessToken;  
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

axios.interceptors.request.use: 모든 HTTP request 전에 호출 되는 interceptor

(async config =>: config는 axios 요청의 설정을 포함하는 객체
(url, headers, method, params, data)

{ const excludedUrls = [ : 특정 url을 제거

if (excludedUrls.some(url => config.url.includes(url))) { return config; }: 요청한 url이 제외 url에 포함되어 있다면 요청 구성을 그대로 반환하여 인터셉터를 종료

let accessToken = localStorage.getItem('access');: 재할당이 가능해야 하므로 변수인 let으로 선언

const tokenData = parseJwt(accessToken);: 처음에 서술한 Access Token 디코딩 함수 호출 -> Payload 부분만 선언

const now = Math.ceil(Date.now() / 1000);: Math.ceil -> JS 내장 함수로 반올림하는 역할, Date.now는 현재 시간을 밀리초로 나타내고, 밀리초를 1000으로 나누어 초 단위로 변경

if (tokenData.exp < now): Payload에 포함되있는 exp를 사용하여 토큰 만료 시간을 확인, 현재 시간과 비교

config.headers['Authorization'] = 'Bearer ' + accessToken; : config headers 부분 Authorization에 Access Token을 추가

# accounts/urls.py
path('login/', views.LoginPageView.as_view(), name="login"),
path('api/token/', views.CustomTokenObtainPairView.as_view(), name="token_obtain"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path('logout/', views.LogoutAPIView.as_view(), name='logout'),

 

# accounts/views.py
class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

class LogoutAPIView(APIView):
    permission_classes = [AllowAny]

    def post(self, request):
        try:
            refresh_token = request.data['refresh']
            token = RefreshToken(refresh_token)
            token.blacklist()

        except Exception as e:
            print(f"Exception: {e}")  # 예외 메시지 출력
        return Response(status=status.HTTP_205_RESET_CONTENT)
 
# accounts/serializers.py
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):

    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        token["user_id"] = user.id
        token["user_nickname"] = user.nickname
        return token

    def validate(self, attrs):
        data = super().validate(attrs)
        data["user_id"] = self.user.id
        data["user_nickname"] = self.user.nickname
        return data

CustomTokenObtainPairSerializer(TokenObtainPariSerializer):
Django 내장 JWT TokenObtainPairSerializer를 상속 받아 우리가
필요한 데이터를 추가하기 위해 Serializer를 Custom

def get_token(cls, user): 로그인 시 response로 보내주는 token 데이터에 user 정보를 담는 함수

def validate(self, attrs): 넣은 데이터들의 유효성 검사

// accounts/login.js
document.getElementById('login-form').addEventListener('submit', function(e) {
    e.preventDefault();

    // CSRF 토큰을 가져옵니다.
    const csrfToken = getCsrfToken();

    axios.post('/api/accounts/api/token/', {
        username: document.getElementById('username').value,
        password: document.getElementById('password').value
    }, {
        headers: {
            'X-CSRFToken': csrfToken
        }
    })
    .then(response => {
        localStorage.setItem('access', response.data.access);
        localStorage.setItem('refresh', response.data.refresh);
        localStorage.setItem('user_id', response.data.user_id);
        localStorage.setItem('user_nickname', response.data.user_nickname);
        window.location.href = '/api/accounts/main/'
    })
    .catch(error => {
        console.error('로그인 실패.', error);
    });
});

위에서 Custom한 Serializer data를 보내주는 view에 연결된 url로
axios.post 요청을 보내, response로 받은 data들을 localStorage에
저장

// base.js
function logout() {
    const refreshToken = localStorage.getItem('refresh');
    const csrfToken = getCsrfToken();
    axios.post('/api/accounts/logout/', { refresh: refreshToken }, {
        headers: {
            'X-CSRFToken': csrfToken
        }
    })
    .then(response => {
        localStorage.removeItem('access');
        localStorage.removeItem('refresh');
        localStorage.removeItem('user_id');
        localStorage.removeItem('user_nickname');
        window.location.href = '/api/accounts/login/';
    })
    .catch(error => {
        console.error('로그아웃 실패.', error);
    });
}
# accounts/views.py LogoutAPIView(APIView) def post
refresh_token = request.data['refresh']
            token = RefreshToken(refresh_token)
            token.blacklist()

token = RefreshToken(refresh_token): RefreshToken() -> Refresh Token의 유효성 검사를 하는 내장 함수

token.blacklist(): 유효성 검사를 마친 Refresh Token을 Blacklist에 추가하여, 다시 사용할 수 없도록 함.

로그아웃 함수에 request가 들어오게 되면 반드시 로그아웃이 처리가 되게 하여, 이미 Refresh Token이 Blacklist에 있던, 만료 상태이던지에 영향을 받지 않고 로그아웃 처리(localStorage의 모든 정보 삭제)를 하도록 함

'JS' 카테고리의 다른 글

Whateversong Profile.js JS 로직 설명  (0) 2025.03.12
Whateversong base.js JS로직 기본 설명  (1) 2025.03.12

+ Recent posts