VueJS

[VueJS] vue-cli + vuex + jwt 로그인 기능 구현하기

SongMinu 2021. 2. 27. 19:22
728x90

공부하기 위해 구매했던 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

 

네비게이션 가드 | Vue Router

네비게이션 가드 이름에서 알 수 있듯이 vue-router가 제공하는 네비게이션 가드는 주로 리디렉션하거나 취소하여 네비게이션을 보호하는 데 사용됩니다. 라우트 탐색 프로세스에 연결하는 방법

router.vuejs.org

 

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

 

프로젝트_v8 Vue Login 기능 만들기

준비물 1. express 서버 키기 2. mysql 서버 키기 3. Vue 서버 키기 *설치 모듈* 서버와 통신하기 위한 axios 설치 상태 관리를 위한 Vuex 설치 Login 기능 만들기 + 인증 1. axios를 main.js 에 넣어서 전역으로..

kdinner.tistory.com

https://kdinner.tistory.com/60

 

Vue - Login 세션 유지하기

사용할 기능들 1. navigation guard - 뷰 라우터 내비게이션 가드 2. axios Intercept - axios 설정 3. Vue cookies 4. vuex 토큰을 이용한 로그인 유지 flow는 아래 사진과 같습니다. 참고 - 쉽게 설명해주셨습..

kdinner.tistory.com

https://focuspro.tistory.com/14

 

Vue.js Jwt 인증 팁? 개인적인 정리

현재 Vue.js(Quasar framework 사용중)을 이용하여 웹개발을 진행중에있다. Quasar framework 를 사용한 이유는 이전 게시글에도 썼지만, SSR(서버사이드 렌더링)을 위함과 Quasar Plugin 등을 사용하기 위해서

focuspro.tistory.com

https://www.javaer101.com/ko/article/2206253.html

 

Axios 인터셉터는 원래 요청을 다시 시도하고 원래 약속에 액세스 - Javaer101

이 기사는 인터넷에서 수집됩니다. 재 인쇄 할 때 출처를 알려주십시오. 침해가 발생한 경우 연락 주시기 바랍니다[email protected] 삭제

www.javaer101.com

https://joshua1988.github.io/web-development/vuejs/vue-router-navigation-guards/

 

(중급) Vue.js 라우터 네비게이션 가드 알아보기

뷰 라우터를 사용할 때 알아두면 좋은 네비게이션 가드 설명. 특정 페이지로 넘어가기 전에 검증 로직 추가하기

joshua1988.github.io

 

 

 

 

 

 

 

반응형