공부하기 위해 구매했던 Vue.js 책을 다 읽을 때쯤 회사에서 vue-cli 기반으로 jwt를 이용해서 로그인 기능과 게시판 리스트까지만 구현을 해보라 해서 만들어봤었다.
다른 부분은 다 쉬웠는데 axios발생이나 컴포넌트 전환 시 accessToken이 없을 경우 토큰을 재발급 하는 과정이 좀 오래 걸렸다.
구글링을 통해 많은 곳에서 참고했지만 원하는 결과가 잘 나오지 않아서 생각보다 많은 시간을 소비했다.
목표
1. vue-cli와 vuex 기반의 웹 애플리케이션에 JWT(Json Web Token)을 이용하여 로그인 기능 구현
2. 로그인시 토큰 값을 쿠키(Cookies)에 저장할 것
3. 토큰 저장시 유효기간이 1분인 accessToken과 1시간인 refreshToken 이렇게 2개의 토큰을 저장할 것
3. 화면 전환, axios 요청을 할 때마다 토큰 값을 체크
- 토큰값 체크 시 accessToken만 없을 경우 refreshToken을 이용해 토큰을 재발급해서 토큰을 새로 저장할 것
- 이 과정에서 사용자는 중간에 끊기지 않고 자연스럽게 웹 사용이 가능하게 하기.
4. 토큰 값 두 개가 모두 없을 경우에는 로그인 페이지로 이동
프로젝트 생성
vue init webpack vue_web
npm axios
npm vuex
프로젝트 구조
중요한 부분은
네비게이션 가드로 url이 변경될 때마다 토큰을 체크하는 src/router/index.js
컴포넌트가 변경될 때 마다 토큰을 체크하는 src/App.vue
axios가 발생할 때마다 interceptors를 이용해서 토큰을 체크하는 src/service/axios.js
토큰 관련 처리 로직이나 게시판 데이터를 불러오는 등 과정을 vuex.store를 모듈 방식으로 처리했고 해당 부분은
src/stroe 안에 있다.
핵심 소스
index.js
//네비게이션 가드((뷰 라우터로 URL 접근에 대해서 처리할 수 있음)
router.beforeEach( async(to, from, next) => { //여기서 모든 라우팅이 대기 상태가 됨
/**
* to: 이동할 url 정보가 담긴 라우터 객체
* from: 현재 url 정보가 담긴 라우터 객체
* next: to에서 지정한 url로 이동하기 위해 꼭 호출해야 하는 함수
* next() 가 호출되기 전까지 화면 전환되지 않음
*/
if(VueCookies.get('accessToken')===null && VueCookies.get('refreshToken') !== null){
//refreshToken은 있고 accessToken이 없을 경우 토큰 재발급 요청
await store.dispatch('refreshToken');
}
if (VueCookies.get('accessToken')){
//accessToken이 있을 경우 진행
return next();
}
if(VueCookies.get('accessToken')===null && VueCookies.get('refreshToken') === null){
//2개 토큰이 모두 없을 경우 로그인페이지로
return next({name: 'Login'});
}
return next();
})
웹의 url이 변경될 때마다 네비게이션 가드를 이용해 토큰을 체크한다.
네비게이이션 가드에 대해선 아래 링크를 참고
router.vuejs.org/kr/guide/advanced/navigation-guards.html
App.vue
export default {
name: 'App',
created() {
//메인 컴포넌트를 렌더링하면서 토큰체크
let token = this.$store.getters.getToken;
if (token.access == null && token.refresh == null) { //다 없으면 로그인 페이지로
//이미 로그인 페이지가 떠있는 상태에서 새로 고침하면 중복 에러 떠서 이렇게 처리함
this.$router.push({name: 'Login'}).catch(() => {});
}
},
components: {
'topMenu': menu
}
}
</script>
먼저 App.vue에서 토큰 체크를 하게끔 해놨다.
토큰이 모두 없을 경우에는 로그인 창으로 이동한다.
index.js에 네비게이션 가드 로직이 있어서 이게 필요 없을 것 같긴 했는데 url 변경이 아닌
컴포넌트만 변경되는 경우엔 이게 없으면 토큰이 없으면 안 돼야 하는 게 되길래 넣어둠.
store/module/login.js
import axios from "axios";
import VueCookies from 'vue-cookies';
//로그인 처리 관련 저장소 모듈
export const login = {
state: {
host: 'http://192.168.1.29:3000',
accessToken: null,
refreshToken: null
},
mutations: {
loginToken (state, payload) {
VueCookies.set('accessToken', payload.accessToken, '60s');
VueCookies.set('refreshToken', payload.refreshToken, '1h');
state.accessToken = payload.accessToken;
state.refreshToken = payload.refreshToken;
},
refreshToken(state, payload) { //accessToken 재셋팅
VueCookies.set('accessToken', payload.accessToken, '60s');
VueCookies.set('refreshToken', payload.refreshToken, '1h');
state.accessToken = payload;
},
removeToken () {
VueCookies.remove('accessToken');
VueCookies.remove('refreshToken');
},
},
getters: {
//쿠키에 저장된 토큰 가져오기
getToken (state) {
let ac = VueCookies.get('accessToken');
let rf = VueCookies.get('refreshToken');
return {
access: ac,
refresh: rf
};
}
},
actions: {
login: ({commit}, params) => {
return new Promise((resove, reject) => {
axios.post('/v1/auth/login', params).then(res => {
commit('loginToken', res.data.auth_info);
resove(res);
})
.catch(err => {
console.log(err.message);
reject(err.message);
});
})
},
refreshToken: ({commit}) => { // accessToken 재요청
//accessToken 만료로 재발급 후 재요청시 비동기처리로는 제대로 처리가 안되서 promise로 처리함
return new Promise((resolve, reject) => {
axios.post('/v1/auth/certify').then(res => {
commit('refreshToken', res.data.auth_info);
resolve(res.data.auth_info);
}).catch(err => {
console.log('refreshToken error : ', err.config);
reject(err.config.data);
})
})
},
logout: ({commit}) => { // 로그아웃
commit('removeToken');
location.reload();
}
}
}
토큰 관련해서 처리하는 건 모두 여기에 있고 로그인, 토큰 재발급, 로그아웃, 토큰 반환 등 토큰 관련 로직이 필요한 파일들은 이 저장소를 사용한다.
actions 쪽에 promise를 사용한 이유는 axios를 사용해서 토큰 재발급을 할 때 interceptors에서 재발급 요청을 한 후 처리하는데 비동기 통신이어서 무한으로 요청을 때리는 현상이 발생하면서 웹이 먹통이 됐었다.
그래서 promise를 사용해서 처리했다. (actions 안에서 비동기 통신을 하는 부분은 promise로 처리 후 받는 쪽에선 async/await으로 처리해주는게 좋긴 하다.)
위 소스에서 state의 토큰 부분과 mutation의 state에 토큰 값을 저장하는 건 사실상 안 씀.
service/axios.js
//request 설정
axios.interceptors.request.use(async function (config) {
if (config.retry==undefined) { //
/**
* axios 요청 중에 accessToken 만료시 재발급 후 다시 요청할 땐
* 기존 요청 정보에서 retry=true만 주가되고
* 나머지는 그대로 다시 요청하기 때문에 url이 이상해져서 이렇게 나눔
*/
config.url = store.state.login.host + config.url; //host 및 url 방식 수정필요
}
//헤더 셋팅
config.timeout = 10000;
config.headers['x-access-token'] = VueCookies.get('accessToken');
config.headers['x-refresh-token'] = VueCookies.get('refreshToken');
config.headers['Content-Type'] = 'application/json';
// console.log(config);
return config;
}, function (error) {
console.log('axios request error : ', error);
return Promise.reject(error);
});
//response 설정
axios.interceptors.response.use(
function (response) {
try {
return response;
} catch (err) {
console.error('[axios.interceptors.response] response : ', err.message);
}
},
async function (error) {
try {
//에러에 대한 response 정보
const errorAPI = error.response.config; //요청했던 request 정보가 담겨있음
//인증에러 및 재요청이 아닐 경우... (+재요청인데 refreshToken이 있을 경우)
if (error.response.status == 401 && errorAPI.retry==undefined && VueCookies.get('refreshToken')!=null) {
errorAPI.retry = true; //재요청이라고 추가 정보를 담음
await store.dispatch('refreshToken'); //로그인 중간 저장소에 있는 토큰 재발급 action을 실행
return await axios(errorAPI); //다시 axios 요청
}
} catch (err) {
console.error('[axios.interceptors.response] error : ', err.message);
}
return Promise.reject(error);
})
여기가 가장 중요한 부분인데.
여기를 완성하는데 너무 많은 시간을 사용했다.
설명이 잘 되어 있는 블로그 등은 많았는데 내가 제대로 이해를 하지 못해서 더 오래 걸리기도 했다.
특히 axios가 발생할 때 accessToken이 없을 경우 다시 토큰을 재발급한 후 요청을 해야 할 때
난 당연히 request 쪽에서 해야 되는 줄 알고 계속 request 쪽에서 처리하려고 했었다.
구글링을 계속하다 보니 response에서 처리를 해줘야 한다고 했었다.
axios발생 시 request에서 헤더에 토큰 값을 넣어주고 이상이 없다?
그럼 response에서 response를 그대로 반환해준다.
에러가 발생했을 경우 status가 401이고 재요청한 axios가 아니고 refreshToken이 있을 경우에는,
다시 axios를 요청할 건데 재요청한 거라고 구분할 retry = true를 넣어주고, 토큰을 재발급한 후 다시 요청을 한다.
이렇게 정상적으로 흐른다면 사용자는 중간에 끊김 없이 이용이 가능하다.
로그인 저장소의 actions안에 있는 refreshToken을 promise로 비동기를 처리하지 않으면 저기서 요청을 미친 듯이 하게 되는데 그럼 웹이 그대로 먹통이 된다.
주의해야 한다.
공부한지 얼마 되지 않은 시점에서 많은 구글링과 많은 삽질을 통해 만든 거다 보니 부족함이 많은 소스 입니다.
그래도 완벽하진 않지만 원하는 기능은 구현은 되니 이런 방법으로도 했구나... 정도로만 참고해주세요.
잘못된 부분이 있다면 지적해주시면 감사드립니다...
소스는 github.com/smw0807/minu_vuejs/tree/master/vue_web/web 여기에 올려 두었고 최대한 주석들을 달아 놨기 때문에 다른 부분에 대한 설명은 작성하지 않았습니다.
만들면서 참고했었던 링크들
kdinner.tistory.com/31?category=310445
https://kdinner.tistory.com/60
https://focuspro.tistory.com/14
https://www.javaer101.com/ko/article/2206253.html
https://joshua1988.github.io/web-development/vuejs/vue-router-navigation-guards/
'VueJS' 카테고리의 다른 글
[VueJS] Vuetify v-text-field 유효성 검사 (rules) (9) | 2021.04.20 |
---|---|
[VueJS] Vuetify v-select에서 text, value값 가져오기 (0) | 2021.04.18 |
[VueJS] vue : 이 시스템에서 스크립트를 실행할 수 없으므로.... (0) | 2021.02.02 |
[VueJS] 컴포넌트 props와 리터럴속성 그리고 검증 (0) | 2021.01.27 |
[VueJS] Component template requires a root element, rather than just text. (0) | 2021.01.24 |