레스트 API 서버로 요청을 보내기 전에 Access-Token이 만료되면 재발급되도록 처리가 되었습니다. 그런데 모든 Vue의 메서드에서 레스트 API 서버로 요청을 보내기 전에 Access-Token이 만료되었는지 확인해야 할까요?
그렇지 않습니다.
axios(액시오스)의 인터셉터(interceptor)를 이용하면 레스트 API 서버로 요청(request)을 보내기 전과 응답(response) 받고 처리 전에 공통으로 처리할 수 있습니다.
다음은 axios(액시오스)의 인터셉터(interceptor) 기본 구조입니다.
// Add a request interceptor
// 요청 인터셉터 추가
axios.interceptors.request.use(function (config) {
// Do something before request is sent
// 요청이 전달되기 전에 처리
return config;
}, function (error) {
// Do something with request error
// 요청 오류에 대한 처리
return Promise.reject(error);
});
// Add a response interceptor
// 응답 인터셉터 추가
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// 응답 상태코드가 2xx 범위안에 있으면 트리거됩니다.
// Do something with response data
// 응답 데이터로 처리
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// 응답 상태코드가 2xx 범위를 벗어나면 트리거됩니다.
// Do something with response error
// 응답 오류에 대한 처리
return Promise.reject(error);
});
참고로, 응답 상태 코드 2xx(성공) 범위는 다음과 같습니다.
200(성공), 201(작성됨), 202(허용됨), 203(신뢰할 수 없는 정보), 204(콘텐츠 없음), 205(콘텐츠 재설정), 206(일부 콘텐츠), 207(다중 상태, RFC 4918), 208(이미 보고됨, RFC 5842), 226 IM Used (RFC 3229)
Node.js 레스트 API 서버를 먼저 실행시킵니다.
콘솔을 실행하고 Node.js 레스트 API 서버 프로젝트가 있는C:\workspaces\nodeserver\testrestapi 폴더로 이동합니다. 그리고 npm run 명령어를 실행합니다.
npm run start
axios(액시오스)의 인터셉터(interceptor) 요청(request) 추가하기
axios(액시오스)의 인터셉터(interceptor) 추가 방법은 두 가지가 있습니다.
첫 번째는 폴더에 있는 main.js에 추가하는 방법입니다.
axios를 app.config.globalProperties에 추가하기 전에 인터셉터(interceptor)를 추가합니다.
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import axios from 'axios';
import store from './store';
const app = createApp(App).use(router).use(store);
axios.interceptors.request.use(function (config) {
// 요청이 전달되기 전에 처리
return config;
}, function (error) {
// 요청 오류에 대한 처리
return Promise.reject(error);
});
app.config.globalProperties.axios = axios;
app.mount('#app');
두 번째는 axios를 생성하는 방법입니다.
모듈화 된 작업을 위해 두 번째 방법으로 하겠습니다.
1. C:\workspaces\nodeserver\testvue\src 폴더에 axios 폴더를 생성하고 index.js 파일을 생성합니다.
2. index.js 파일을 오픈하여 코딩합니다.
Access-Token의 만료 확인은 레스트 API 서버로 요청을 보내기 전에 해야 하기에 axios.interceptors의 request로 처리합니다. 테스트를 위해 콘솔 로그를 출력합니다.
import axios from 'axios';
axios.interceptors.request.use(function (config) {
// 요청이 전달되기 전에 처리
console.log("axios.interceptor");
return config;
}, function (error) {
// 요청 오류에 대한 처리
return Promise.reject(error);
});
export default axios;
3. C:\workspaces\nodeserver\testvue\src\main.js에서 import 한 axios를 생성한 axios로 수정합니다.
import axios from 'axios';
import axios from './axios'; --> 수정
4. 콘솔을 실행하고 Vue 프로젝트가 있는 C:\workspaces\nodeserver\testvue 폴더로 이동합니다. 그리고 콘솔에서 npm run 명령어를 실행합니다.
npm run serve
5. 웹 브라우저에서 "http://localhost:8080/board"를 입력합니다. 그러면 콘솔에 "axios.interceptor"가 출력됩니다.
그리고 쓰기 버튼을 클릭하고 Login 페이지에서 로그인한 후에도 콘솔에 "axios.interceptor"가 출력됩니다.
레스트 API 서버로 보내는 모든 요청 전에 호출되는 것을 확인할 수 있습니다.
6. C:\workspaces\nodeserver\testvue\src\axios\index.js 파일을 오픈하여 C:\workspaces\nodeserver\testvue\src\views\BoardWrite.vue 파일에서 boardSaveClick() 메서드에 추가했던 accessToken의 재발급 처리 로직을 복사하여 추가합니다.
그리고 accessToken이 재발급된 후 config.headers에 재발급된 accessToken으로 수정합니다.
그 이유는 axios.defaults.headers.common['Access-Token']에 설정하면 axios 요청을 만들기 전에 기본 값으로 가져옵니다. 그래서 axios.defaults.headers.common['Access-Token']를 수정하여도 axios.interceptors.request에서는 이전 값이 유지됩니다. 그래서 axios.interceptors.request에서 전달받은 condig를 이용하여 config.headers['Access-Token']으로 처리해야 합니다.
accessToken이 있어야 만료를 확인할 수 있음으로 로그인이 되어있는지 확인합니다.
그리고 동기화 처리를 위해 async와 await를 사용합니다.
import axios from 'axios';
import store from '@/store';
axios.interceptors.request.use(async function (config) {
// 요청이 전달되기 전에 처리
// 로그인되었는지 확인한다.
var isLogin = store.getters['loginStore/isLogin'];
if (isLogin) {
// accessToken이 만료되었는지 확인합니다.
var isAccessTokenExpire = store.getters['loginStore/isAccessTokenExpire'];
if (isAccessTokenExpire) {
// accessToken이 만료되면 토큰 재발급을 진행합니다.
try {
// accessToken 재발급 요청
await store.dispatch("loginStore/doRefreshToken");
// 재발급된 accessToken으로 해더를 수정합니다.
config.headers['Access-Token'] = store.state.loginStore.accessToken;
} catch(err) {
alert("다시 로그인을 해주시기 바랍니다.\n" + err.response.data.errormessage);
}
}
}
return config;
}, function (error) {
// 요청 오류에 대한 처리
return Promise.reject(error);
});
export default axios;
7. C:\workspaces\nodeserver\testvue\src\views\BoardWrite.vue 파일에서 boardSaveClick() 메서드에 추가했던 accessToken의 재발급 처리 로직을 삭제합니다. boardSaveClick() 메서드에 있는 async도 삭제합니다.
boardSaveClick() {
if (this.subject == "") {
alert("제목을 입력하세요.");
this.$refs.subjectInput.focus();
return;
} else if (this.content == "") {
alert("내용을 입력하세요.");
this.$refs.contentTextarea.focus();
return;
}
var result = confirm("등록하시겠습니까?");
if (result) {
let boardItem = { subject : this.subject, content : this.content };
this.axios.post("http://localhost:9000/boards", boardItem).then((res)=>{
console.log(res);
if (res.data.success == true) {
alert("등록되었습니다.");
this.$router.push({name : 'BoardList'});
} else {
alert("등록되지 않았습니다.");
}
}).catch((err) => {
console.log(err);
alert("등록되지 않았습니다.");
});
}
}
8. C:\workspaces\nodeserver\testvue\src\store\modules\login.js 파일에서 actions에 있는 doRefreshToken() 메서드를 수정해야 합니다.
그 이유는 axios.interceptors.request를 추가하여 axios로 전송하면 다시 axios.interceptors.request가 호출되기 때문에 무한 루프가 발생됩니다. 그래서 axios의 인스턴스(instance)를 새로 만들어서 처리해야 합니다.
doRefreshToken() 메서드에서 사용할 axios의 새로운 인스턴스(instance)를 생성합니다.
import axios from 'axios';
const axiosRefresh = axios.create(); --> 추가
const loginStore = {
doRefreshToken() 메서드에서 axios를 새로 생성한 axiosRefresh로 수정합니다.
// Access-Token를 갱신합니다.
async doRefreshToken({commit, state}) {
let result = false;
let resultErr = null;
if (state.accessToken != "") {
let token = { id: state.memberId, accessToken : state.accessToken, refreshToken : state.refreshToken };
try {
let res = await axiosRefresh.post("http://localhost:9000/members/refresh", token); --> 수정
if (res.data.success == true) {
console.log("Access-Token이 갱신되었습니다.");
commit('setAccessToken', res.data.accessToken);
console.log(res.data.accessToken);
axios.defaults.headers.common['Access-Token'] = res.data.accessToken;
result = true;
} else {
console.log("Access-Token이 갱신되지 않았습니다.");
let err = new Error("Request failed with status code 401");
err.status = 401;
err.response = {data:{"success":false, "errormessage":"Access-Token이 갱신되었습니다."}};
resultErr = err;
}
} catch(err) {
if (!err.response) {
err.response = {data:{"success":false, "errormessage":err.message}};
}
resultErr = err;
}
} else {
let err = new Error("Access-Token does not exist");
err.status = 401;
err.response = {data:{"success":false, "errormessage":"Access-Token이 없습니다."}};
resultErr = err;
}
return new Promise((resolve, reject) => {
if (result) {
resolve();
} else {
reject(resultErr);
}
});
}
9. 다시 웹 브라우저에서 "http://localhost:8080/board"를 입력합니다. 그리고 쓰기 버튼을 클릭합니다. Login 페이지에서 아이이와 패스워드를 입력하면 게시물 작성 페이지로 이동됩니다. 테스트를 위해 제목에 "테스트 제목4", 내용에 "테스트 내용4"를 입력하고 10분 후 등록 버튼을 누릅니다. (만약 오류가 발생하면 Vue 종료하고 다시 실행합니다.)
10. 그러면 토큰이 만료되어 콘솔에 "token expired"이 출력됩니다. 그리고 토큰이 재발급되면서 "Access-Token이 갱신되었습니다."라고 출력됩니다.
axios(액시오스)의 인터셉터(interceptor)를 이용하여 Access-Token이 자동으로 재발급되게 되었습니다.