main.jsnpm run serve 실행 시 가장 먼저 실행되는 js 파일App.vue를 준비함app 을 index.html에 마운트해서 화면을 띄울 예정<div id="app"></div> 여기에 App.vue를 마운트 함index.html은 쉽게 말해 App.vue가 들어갈 껍데기에 해당
import { createApp } from 'vue' // Vue 3 애플리케이션 생성 함수
import { createPinia } from 'pinia' // 뷰의 상태 관리 라이브러리
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
// 루트 컴포넌트인 App.vue 파일을 import.
import App from './App.vue'
// Vue Router 설정 파일을 import.
import router from './router'
// Vue 애플리케이션 인스턴스 생성 - App.vue 기반 초기화
const app = createApp(App)
app.use(createPinia()) // pinia 상태 관리 기능 연결
app.use(router) // Vue Router 연결
app.mount('#app') // index.html에 app 마운트 - 화면이 보이기 시작!
App.vue<RouterView></RouterView>router/index.js 에 컴포넌트들의 라우팅 경로를 미리 등록해둠<template>
<RouterView></RouterView>
</template>
<script setup>
import { useAuthStore } from './stores/auth';
const authStore = useAuthStore();
authStore.checkLogin();
</script>
전체적인 흐름
main.js가 먼저 실행 →App.vue를 준비 →App.vue가index.html에 마운트 됨 →Router를 통해 URL 별 컴포넌트에 연결됨 → 페이지 접속
// 로그인 함수
const login = async (loginData) => {
// loginData -> LoginForm.vue에서 받아옴
console.log(loginData);
try {
// 사용자 입력 loginData를 해당 엔드포인트로 POST 요청을 보냄
const response = await apiClient.post('/bonbon/user/login', loginData);
// 로그인에 성공하는 경우,
if(response.status === 200) {
console.log(response);
const userNameResponse = await apiClient.get(
'/bonbon/user',
{
headers: {
'Authorization': `Bearer ${response.data.accessToken}`
}
}
);
console.log(userNameResponse.data);
const parseToken = parseJwt(response.data.accessToken);
// 토큰들을 로컬 스토리지에 저장 + 사용자 정보도 로컬에 저장함
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
localStorage.setItem('userInfo', JSON.stringify({
username: parseToken.username,
name: userNameResponse.data.name,
userImage: userNameResponse.data.userImage,
role: parseToken.role
}));
// userInfo 객체 업데이트
Object.assign(userInfo, JSON.parse(localStorage.getItem('userInfo')));
console.log(userInfo.data);
// 로그인 상태 변경
isLoggedIn.value = true;
// 홈 페이지로 리다이렉트 함
router.push({name: 'main'});
}
} catch (error) {
// 로그인 실패 처리 -> 에러 핸들링
if (error.response.data.code === 400) {
alert(error.response.data.message);
} else {
// 401 이외의 오류 발생 시 일반적인 에러 메시지 표시
alert(error.response.data.message);
// alert('에러가 발생했습니다.');
}
}
};
isLoggedIn : 로그인 되어 있는지 먼저 확인Token을 가져온다 → 토큰이 유효한 경우 server에 Post 방식으로 logout 요청을 보내고, 로그아웃이 제대로 되서 204 응답을 받으면 localstorage에 있는 사용자 정보를 지우고 isLoggedIn을 false로 바꾼 뒤, router.push({name: 'login'});로 login 페이지로 이동시킨다. // 사용자 로그아웃 시
const logout = async () => {
if(!isLoggedIn){
return;
}
try {
// localStorage에서 accessToken 먼저 가져오기
const accessToken = localStorage.getItem('accessToken');
console.log(accessToken);
if (!accessToken || isInvalidAccessToken(accessToken)) {
// accessToken이 유효하지 않는 경우, 토큰이 존재하지 않는 경우
if(isLoggedIn.value){
alert('다시 로그인해 주세요.');
logoutUser();
}
return;
}
// 토큰이 유효한 경우 -> 로그아웃 api 호출
const response = await apiClient.post(
'/bonbon/user/logout',
null,
{
headers: {
'Authorization': `Bearer ${accessToken}`
},
_skipInterceptor: true
}
);
// 로그아웃이 잘 된 경우,
if (response.status === 204) {
logoutUser();
}
} catch (error) {
logoutUser();
}
};
Header에 같이 담아 보내는 AccessToken이 만료되었을 때 자동으로 Refresh 해주는 로직이다.server에서 401응답(토큰 만료)을 보낼 경우 응답 interceptor에서 서버에 refresh를 요청한다.refreshToken도 만료된 경우 : 로그아웃refreshToken 유효 : accessToken 갱신 + 기존의 요청 재전송 / 실행 accessToken은 로그인 때와 마찬가지로 localStorage에 저장된다.// 서버에서 도착한 HTTP 응답(response) 인터셉터
apiClient.interceptors.response.use(
(response) => {
// 평범한 response가 온 경우, 그냥 response 그대로 반환
return response;
},
// 비동기 함수
async (error) => {
// 이전 요청에 대한 config 객체
const originalRequest = error.config;
console.log(error);
if (
originalRequest.url === '/bonbon/user/refresh' // 이미 재시도 한 요청
) {
const authStore = useAuthStore();
authStore.logout();
console.log("refreshToken도 만료 → 바로 로그아웃");
return;
}
// 토큰이 만료되어 401 에러가 발생한 경우, retry 한 적이 없는 경우
if (error.response.status === 401 && !originalRequest._retry) {
// 무한 요청 재시도를 방지하기 위한 체크 변수
originalRequest._retry = true; // 객체에서 동적으로 추가된 변수 -> 응답 인터셉터 내에서 직접 추가됨
try {
// localStorage에서 refreshToken을 가져옴
const refreshToken = localStorage.getItem('refreshToken');
// refreshToken이 존재하지 않는 경우~~~~ -> 무한 루프 방지ㅎㅎ
if (!refreshToken) {
// const authStore = useAuthStore();
// authStore.logout();
return Promise.reject(error); // 리프레시 토큰이 없으면 바로 에러 반환
}
// 이 토큰을 Authorization 헤더에 Bearer ${refresh} 형태로 담아서 해당 엔드포인트에 POST 요청 전송
// apiClient.post -> axios의 POST 메서드 호출 코드
// await : Promise가 해결될 때까지 기다리고, 값을 반환
// -> refreshToken을 이용해 새로운 AccessToken을 얻기 위해 서버에 비동기 요청을 보냄
const response = await apiClient.post(
'/bonbon/user/refresh', // 해당 URL로 post 요청을 보낼거다
null, // 근데 Data는 없다
{ // config 설정은 이러하다. -> 헤더에 Bearer ${refreshToken} 형태로 토큰을 담아서 보낼 예정이다.
headers: {
'Authorization': `Bearer ${refreshToken}`
},
_skipInterceptor: true
}
);
// 새로운 accessToken 받기
const accessToken = response.data.accessToken;
// 새 액세스 토큰을 로컬 스토리지에 저장
localStorage.setItem('accessToken', accessToken);
const parsedToken = parseJwt(accessToken);
const authStore = useAuthStore();
authStore.isLoggedIn = true;
authStore.userInfo.username = parsedToken.username;
authStore.userInfo.role = parsedToken.role;
// 원래 요청을 재시도
return apiClient(originalRequest);
} catch (error) {
// 리프레시 토큰이 만료된 경우, 로그아웃 처리
// const authStore = useAuthStore();
const authStore = useAuthStore();
authStore.logout();
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
Vue.js와 Spring을 이용한 Jwt Token 방식 로그인 로직을 완성했다. 이제 화면을 구성해봐야겠지..