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

    1
    2
    3
    4
    5
    6
    {
    "uid": "회원가입 할 계정에 대한 아이디",
    "password": "회원가입 할 계정에 대한 비밀번호",
    "role": "회원가입 할 계정에 대한 역할",
    "position": "회원가입 할 계정에 대한 포지션"
    }
  • Response Data

    1
    2
    3
    4
    5
    {
    "status": 200,
    "message": "Success",
    "data": {}
    }

Signin API

  • POST /auth/signin

  • Request Data

    1
    2
    3
    4
    {
    "uid": "가입시 기재한 아이디 정보",
    "password": "가입시 기재한 비밀번호 정보"
    }
  • Response Data

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "status": 200,
    "data": {
    "uid": "가입시 기재한 아이디 정보",
    "role": "가입시 기재한 역할에 대한 정보",
    "position": "가입시 기재한 포지션에 대한 정보",
    "accessToken": "엑세스 토큰에 대한 정보",
    "refreshToken": "리프레시 토큰에 대한 정보"
    },
    "message": "User information matched."
    }

identification API

  • GET /auth/me

  • Response Data
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "status": 200,
    "data": {
    "uid": "가입시 기재한 아이디 정보",
    "upk": "가입시 기재한 임의로 부여된 계정에 대한 Primary key",
    "role": "가입시 기재한 포지션에 대한 정보",
    "position": "가입시 기재한 포지션에 대한 정보",
    "iat": 1537100288,
    "exp": 1537100348
    },
    "message": "Success"
    }

reissue access token API

  • GET /auth/me

  • Response Data
    1
    2
    3
    4
    5
    6
    7
    {
    "status": 200,
    "message": "Success",
    "data": {
    "accessToken": "재갱신된 access token 정보"
    }
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<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 화면이다.

/images/vue/vue-jwt-authenicatie-example01.png

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<template>
<div class="hello">
<h1>{{ msg }}</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>

/images/vue/vue-jwt-authenicatie-example02.png

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

1
2
3
4
5
6
7
8
9
10
11
{
"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 를 통해 해당 데이터를 요청하면 아래와 같이 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
{
"status": 200,
"data": {
"uid": "Martin",
"upk": 9,
"role": "Front-end Developer",
"position": "member",
"iat": 1537106141,
"exp": 1537106201
},
"message": "Success"
}

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

1
2
3
4
5
{
"data": {},
"message": "This token is invalid.",
"status": 401
}

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

현재 이커머스회사에서 frontend 개발자로 업무를 진행하고 있는 Martin 입니다. 글을 읽으시고 궁금한 점은 댓글 혹은 메일(hoons0131@gmail.com)로 연락해주시면 빠른 회신 드리도록 하겠습니다. 이 외에도 네트워킹에 대해서는 언제나 환영입니다.:Martin(https://github.com/martinYounghoonKim
S3를 이용한 웹 어플리케이션 올리기
Chrome extension 을 위한 manifest.json 옵션