Whateversong base.js JS로직 기본 설명
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
- CSRF 공격
- 웹 애플리케이션의 취약점을 악용하여 사용자의 권한을 도용하는 공격 방식
- 사용자의 의도와는 상관 없이 공격자가 원한 특정한 요청을 서버에 전송
- CSRF Token의 역할
- CSRF 공격을 방지
- 사용자가 폼을 제출할 때마다 함께 전송 → 서버는 요청이 정당한 사용자가
- 의도적으로 보낸 것인지 확인
- 동작 방식
- 토큰 생성
- 사용자가 웹 애플리케이션에 접근할 때(페이지에 접속, 특정 페이지를 로드할 때) 서버에서 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는 점(.)으로 구분된 세 부분으로 구성:
- Header: 토큰 유형과 서명 알고리즘 정보를 포함
- Payload: 클레임(claim)이라고 하는 **사용자 정보나 기타 데이터**를 포함
- 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 라이브러리
- 역할
- JavaScript에서 **HTTP 요청**을 보낼 때 사용하는 라이브러리
- 특히, **비동기적**으로 서버와 통신할 수 있는 기능을 제공하여 웹 애플리케이션에서 널리 사용
- 간결한 API와 다양한 기능을 통해 HTTP 요청을 쉽게 처리
- 특징
- Promise 기반:
- Axios는 **Promise API**를 사용, 이는 비동기 작업을 더 쉽게 관리하고,
.then() 및 .catch() 구문을 통해 요청의 성공과 실패를 처리
- Axios는 **Promise API**를 사용, 이는 비동기 작업을 더 쉽게 관리하고,
- 요청 및 응답 데이터 변환:
- 요청을 보내기 전에 데이터를 자동으로 **JSON 문자열로 변환**하거나,
서버로부터 응답받은 데이터를 자동으로 **JavaScript 객체**로 변환
- 요청을 보내기 전에 데이터를 자동으로 **JSON 문자열로 변환**하거나,
- 인터셉터(Interceptors):
- 요청이나 응답을 가로채고, 이를 수정하거나 처리
이를 통해 로깅, 권한 부여, 에러 처리 등을 중앙 집중식으로 관리
- 요청이나 응답을 가로채고, 이를 수정하거나 처리
- 자동화된 CSRF 보호:
- CSRF(Cross-Site Request Forgery) 토큰을 자동으로 설정하고, 요청 **헤더**에 포함
- 취소 토큰:
- 요청을 **취소**할 수 있는 기능을 제공
이는 사용자가 불필요한 요청을 중단하거나, 네트워크 상태에 따라 요청을 제어
- 요청을 **취소**할 수 있는 기능을 제공
- Promise 기반:
- 설치
<!-- 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
- addEventListener는 eventType과 eventHandler를 인수로 받는다.
- eventType - 'click', 'submit', 'load', 'keydown' 등
- eventHandler - 이벤트가 발생했을 때 실행되는 함수
- function(e) or function(event)
eventHandler 함수는 일반적으로 이벤트 객체(e, event)를 인수로 받는다.
이 이벤트 객체는 이벤트에 대한 다양한 정보를 포함하고 있다.
- function(e) or function(event)
- e.preventDefault()
- 이벤트의 기본 동작을 막는 것,
위 함수를 예로 들면, 'click' 시 페이지가 새로고침 되는 동작을 막는다.
- 이벤트의 기본 동작을 막는 것,
JQuery
- 정의
- JavaScript를 더 쉽고 간편하게 사용할 수 있도록 도와주는 경량의 JavaScript 라이브러리
- DOM(Document Object Model) 조작, 이벤트 처리, 애니메이션, AJAX 요청 등을
단순화
- 사용 이유
- `/api/accounts/profile/${userId}/` : URL을 JQuery로 작성함으로써,
userId를 받아와 URL주소에 넣어주어 속성을 동적으로 설정함
- `/api/accounts/profile/${userId}/` : URL을 JQuery로 작성함으로써,
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
사용 이유
- 표준 및 관례: 많은 웹 프레임워크(예: Django, Flask)는 CSRF 토큰을 HTTP 헤더의
X-CSRFToken 필드에 담아 전송하도록 설계,
이는 클라이언트와 서버 간의 일관된 보안 메커니즘을 유지하기 위함 - 명확한 목적: X- 접두어는 일반적으로 비표준 헤더를 나타내며,
CSRF 토큰을 포함한 헤더는 HTTP 표준 헤더가 아니므로 X-를 사용하여 명확히 구분 - 보안 및 가독성: 헤더에 CSRF 토큰을 포함하면, URL 또는 요청 본문에 포함하는 것보다 더 안전,
이는 토큰이 쉽게 노출되지 않도록 하여 보안을 강화하는 데 도움,
또한, 헤더에 포함하면 요청의 의도를 명확하게 전달