[VueJS] VueJS를 이용한 JWT 인증 기반 CRUD 구현하기

JWT 인증 기반의 Backend 와 연동하기

요즘은 많은 회사들이 개발 트렌드가 MSA 으로 가고 있는 듯 하다. 얼마 전, 카카오 컨퍼런스 If Kakao 에 가서 가장 관심 깊게 보았던 내용 역시 카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개 에 대한 세션이었다. 아무래도 현재 재직 중인 회사(여기어때/호텔타임 을 서비스 하고 있는 위드이노베이션) 에서의 관심사가 가장 잘 반영된 세션이었던 터라 그랬던 것 같다. 그래서 이참에 간단하게 NodeJS 를 기반으로 JWT 인증 방식을 통해 한 번 구현해볼까 한다.

관련해서 레파지토리를 여기 를 클릭하면 Backend 와 Frontend 디렉토리로 분리되어 있는 것을 확인 할 수 있다. 일단 이 포스팅에서는 Backend 구현체에 대한 이야기를 하지 않는다.(이 후 별도로 구현 방법에 대해서 포스팅 하게 된다면 링크를 해당 포스팅에도 걸어두겠다.) 혹시나 해당 예제를 직접 구현을 해보고 싶다면 README.md 를 참고해서 Database 를 세팅해주면 예제를 실습할 수 있다.

일단 회원가입과 로그인만 구현을 해보겠다. 로그인과 회원가입에 대한 API 는 다음과 같다.

Signup API

  • POST /auth/signup

  • Request Data
    {
      "uid": "회원가입 할 계정에 대한 아이디",
      "password": "회원가입 할 계정에 대한 비밀번호",
      "role": "회원가입 할 계정에 대한 역할",
      "position": "회원가입 할 계정에 대한 포지션"
    }
    
  • Response Data
    {
      "status": 200,
      "message": "Success",
      "data": {}
    }
    

Signin API

  • POST /auth/signin

  • Request Data
    {
      "uid": "가입시 기재한 아이디 정보",
      "password": "가입시 기재한 비밀번호 정보"
    }
    
  • Response Data
    {
      "status": 200,
      "data": {
          "uid": "가입시 기재한 아이디 정보",
          "role": "가입시 기재한 역할에 대한 정보",
          "position": "가입시 기재한 포지션에 대한 정보",
          "accessToken": "엑세스 토큰에 대한 정보", 
          "refreshToken": "리프레시 토큰에 대한 정보"
      },
      "message": "User information matched."
    }
    

identification API

  • GET /auth/me

  • Response Data
    {
      "status": 200,
      "data": {
          "uid": "가입시 기재한 아이디 정보",
          "upk": "가입시 기재한 임의로 부여된 계정에 대한 Primary key",
          "role": "가입시 기재한 포지션에 대한 정보",
          "position": "가입시 기재한 포지션에 대한 정보",
          "iat": 1537100288,
          "exp": 1537100348
      },
      "message": "Success"
    }
    

reissue access token API

  • GET /auth/me

  • Response Data
    {
      "status": 200,
      "message": "Success",
      "data": {
          "accessToken": "재갱신된 access token 정보"
      }
    }
    

먼저 로그인을 하기 위해서는 회원가입을 해야 한다. Vue의 컴포넌트는 Vue-bootstrap 을 기반으로 제작하였다.

<template>
    <div>
        <h1>Signup Form</h1>
        <form @submit.prevent="signup">
            <b-form-group label="Enter your user id">
                <b-form-input type="text" v-model="uid"></b-form-input>
            </b-form-group>
            <b-form-group label="Enter your password">
                <b-form-input type="password" v-model="password"></b-form-input>
            </b-form-group>
            <b-form-group label="Enter your position">
                <b-select v-model="position" :options="positionOptions"></b-select>
            </b-form-group>
            <b-form-group label="Enter your role">
                <b-select v-model="role" :options="roleOptions"></b-select>
            </b-form-group>
            <b-button size="lg" variant="success" type="submit">Signup</b-button>
        </form>
    </div>
</template>

<script>
    import axios from 'axios';

    export default {
        name: 'Signup',
        data () {
            return {
                uid: '',
                password: '',
                role: '',
                position: '',
                positionOptions: [
                    { text: '개발자', value: 'developer' },
                    { text: '기획자', value: 'director' },
                    { text: '디자이너', value: 'designer' },
                ],
                roleOptions: [
                    { text: '일반', value: 'member' },
                    { text: '관리자', value: 'admin' },
                ]
            };
        },
        methods: {
            signup () {
                const uid = this.uid;
                const password = this.password;
                const position = this.position;
                const role = this.role;

                if (!uid || !password || !position || !role) {
                    return false;
                }

                axios.post('http://localhost:3000/auth/signup', { uid, password, position, role })
                    .then(res => {
                        if (res.status === 200) {
                            // 성공적으로 회원가입이 되었을 경우
                            this.$router.push({ name: 'Signin' });
                        }
                    });
            }
        }
    };
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    h1, h2 {
        font-weight: normal;
    }

    ul {
        list-style-type: none;
        padding: 0;
    }

    li {
        display: inline-block;
        margin: 0 10px;
    }

    a {
        color: #42b983;
    }

    .btn-lg {
        width: 100%;
    }
</style>

코드는 위와 같고, 아래는 실제 보여지는 View 화면이다.

위에서 설명했듯 API 에서 필요한 필드는 uid, password, position, role 이다. position 과 role 은 지금 당장 로그인할 때는 큰 의미가 없지만, 이후 권한 체크를 하기 위헤서 추가해주었다.

모든 필드를 입력한 후에 로그인을 하면 회원가입이 된다. 물론 서버에서도 field 에 대한 중복 체크 및 validation 체크를 하지만 대체적으로 대부분 회원가입을 할 때는 비밀번호 confirm 필드를 하나 더 줘서 비밀번호 일치 체크를 하나 여기에서는 회원가입 로직이 관심사가 아니므로 그냥 넘어가도록 하겠다.

위와 같이 회원가입이 되었다면 바로 회원가입한 계정으로 로그인을 진행해보겠다. 로그인은 Signin 과 비슷하지만 필드가 아이디와 비밀번호만 존재한다.

<template>
    <div class="hello">
        <h1></h1>
        <form @submit.prevent="signin">
            <b-form-group label="Enter your user id">
                <b-form-input v-model="uid" type="text"></b-form-input>
            </b-form-group>
            <b-form-group label="Enter your password">
                <b-form-input v-model="password" type="password"></b-form-input>
            </b-form-group>
            <b-button size="lg" variant="success" type="submit">Signin</b-button>
        </form>
        <router-link :to="{ name:'Signup' }">Sigiup</router-link>
    </div>
</template>

<script>
    import axios from 'axios';

    export default {
        name: 'Signin',
        data () {
            return {
                msg: 'Welcome to Your Vue.js App',
                uid: '',
                password: '',
            };
        },
        methods: {
            signin () {
                const uid = this.uid;
                const password = this.password;

                if (!uid || !password) {
                    return false;
                }

                axios.post('http://localhost:3000/auth/signin', { uid, password })
                    .then(res => {
                        if (res.status === 200) {
                            alert('로그인 성공');
                            document.cookie = `accessToken=${res.data.data.accessToken}`;
                            axios.defaults.headers.common['x-access-token'] = res.data.data.accessToken;
                            this.$router.push({ name: 'Home' });
                        }
                    })
                    .catch(err => {
                        alert('로그인 실패');
                    })
            }
        }
    };
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    h1, h2 {
        font-weight: normal;
    }

    ul {
        list-style-type: none;
        padding: 0;
    }

    li {
        display: inline-block;
        margin: 0 10px;
    }

    a {
        color: #42b983;
    }

    .btn-lg {
        width: 100%;
    }
</style>

만약 제대로 된 계정으로 로그인을 시도했다면 로그인 성공이라는 alert 창이 뜰 것이고, response 로는 필자와 비슷하게 뜰 것이다.

{
    "status": 200,
    "data": {
        "uid": "Martin",
        "role": "Front-end Developer",
        "position": "member",
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJNYXJ0aW4iLCJ1cGsiOjksInJvbGUiOiJGcm9udC1lbmQgRGV2ZWxvcGVyIiwicG9zaXRpb24iOiJtZW1iZXIiLCJpYXQiOjE1MzcwOTkzMjUsImV4cCI6MTUzNzA5OTM4NX0.iswTCUwlwWUQziiYL20K7e_YGuEHfZuN8oaKmkTc8CA",
        "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJNYXJ0aW4iLCJpYXQiOjE1MzcwOTkzMjUsImV4cCI6MTUzNzcwNDEyNX0.OC83ez1HKR4BxlEfZePRcW_LUZtTnswlBViL2KlPbak"
    },
    "message": "User information matched."
}

여기에서 살펴볼 것은 accessTokenrefreshToken 이다. accessToken 은 자원에 접근할 수 있는 token 이고, refreshToken 은 accessToken 을 갱신하기 위한 토큰이다. 일단 임의로 여기에서는 refreshToken 을 가지고 accessToken 을 계속 갱신 받는 것이 주목적임으로 accessToken 의 주기는 굉장히 짧게 1분으로 해놓았다. 로그인을 한 후에 일단 두 token 을 먼저 cookie 에다가 저장을 한다. 여기까지 왔으면 이제 우리가 원하는 token 에 대한 핸들링만 구현하면 끝이 난다. 일단 로그인을 한 후, API 인증하는 URI /auth/me 를 통해 해당 데이터를 요청하면 아래와 같이 나온다.

{
    "status": 200,
    "data": {
        "uid": "Martin",
        "upk": 9,
        "role": "Front-end Developer",
        "position": "member",
        "iat": 1537106141,
        "exp": 1537106201
    },
    "message": "Success"
}

해당 uri 를 통해서 재갱신을 테스트해 볼 것이다. accessToken 의 경우 유효한 시간을 위에서 말했듯 1분으로 설정을 해두었기 때문에 1분 정도가 지난 후 다시 요청하면 response 가 아래와 같다.

{
    "data": {},
    "message": "This token is invalid.",
    "status": 401
}

해당 accessToken 이 더 이상 유효 하지 않아서이다. 이럴 때, 바로 사용하는 것이 refreshToken 이다. 위와 같이 response 과 왔을 때, 해당 에러를 잡아서 다시 토큰을 재갱신하는 /auth/me 에 요청을 보내면 된다.

Comments