JS

Whateversong base.js JS로직 기본 설명

devjingood 2025. 3. 12. 00:59
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 또는 요청 본문에 포함하는 것보다 더 안전,
    이는 토큰이 쉽게 노출되지 않도록 하여 보안을 강화하는 데 도움,
    또한, 헤더에 포함하면 요청의 의도를 명확하게 전달