티스토리 뷰

Frontend/Vue.js

Vue.js3 Login + JWT 토큰

Mo'Greene 2023. 7. 11. 22:17

서론.

프론트엔드는 하나하나 잘 정리하지 않은 채 작업을 한다면 코드의 양이나 복잡도가 너무나도 높아지는 느낌을 받았다.

그로 인해 상태관리 라이브러리 (Pinia, Vuex..)를 굉장히 효율적으로 사용해야

중복되고 복잡한 코드의 양을 줄일수 있다는 것을 느끼고 있다.

 

당장에 로그인만 하더라도 JWT토큰을 백엔드에서 구현하는 것과 함께 Vue.js 내에서 관리해 줘야 되는 게여간 까다로운 게 아니란 것을 알게 되었다.

 

그렇기에 정리를 해두고 계속 고쳐가는 방향으로 진행하려고 한다.

 


JWT 토큰

https://mo-greene.tistory.com/108

 

Jwt + SpringSecurity + Mybatis 구현

Vue.js 와 SpringBoot를 사용해 게시판 토이프로젝트를 구현하고 있다. CRUD과정은 전부 끝났고 Vue.js와 씨름하며 시간을 보내다가 드디어 로그인 파트로 넘어왔다. https://www.inflearn.com/course/%EC%8A%A4%ED%94

mo-greene.tistory.com

 

백엔드에서 구현한 JWT토큰은 위 블로그에 포스팅해놨다.

특이점은 기존에 JPA로 구현하는 토큰과는 다르게 Mybatis로 회원테이블을 구현했다는 점이고

그 외에는 특이사항이 없을 듯하다.

 


Login.vue

<template>
    <v-card>
        <div class="d-flex align-center justify-center" style="height: 100vh">
            <v-sheet width="400" class="mx-auto">
                <v-form fast-fail ref="loginForm">
                    <v-text-field
                        variant="outlined"
                        v-model="username"
                        label="username"
                        :rules="usernameRules"
                    ></v-text-field>

                    <v-text-field
                        variant="outlined"
                        v-model="password"
                        label="password"
                        :rules="passwordRules"
                    ></v-text-field>

                    <v-btn
                        color="primary"
                        block
                        class="mt-2"
                        @click="validation">Log in</v-btn>
                </v-form>
                <div class="mt-2">
                    <p class="text-body-2">Don't have an account? <a href="#">Sign Up</a></p>
                </div>
            </v-sheet>
        </div>
    </v-card>
</template>


<script setup>
import * as loginApi from '@/api/login/login'
import {ref} from "vue";
import {useLoginStore} from "@/store/login";
import { useRouter } from "vue-router";

const username = ref();
const password = ref();
const loginForm = ref();
const loginStore = useLoginStore();
const router = useRouter();

//login Logic
const login = async () => {
    try {
        // Your login logic here
        const response = await loginApi.login(username.value, password.value);

        if (response.status === 200) {
            await loginStore.setToken(response.data.token);
            localStorage.setItem('access_token', response.data.token)
            await loginStore.getUserInfo();

            await router.push({path: '/'})
        }

    } catch (e) {
        alert(e.response.data.message)
    }
}

//Validation loginForm
async function validation (){
    const { valid } = await loginForm.value.validate();

    if (valid) await login();
    else {
        alert('로그인 양식에 맞지 않습니다.')
    }
}
const usernameRules = ref([
    value => {
        if (value) return true
        return '아이디를 적어주세요.'
    },
])
const passwordRules = ref([
    value => {
        if (value) return true
        return '비밀번호를 적어주세요.'
    },
])
</script>

테스트용 아이디와 비밀번호다.

 

중요한 부분이라고 생각되는 코드는

if (response.status === 200) {
            await loginStore.setToken(response.data.token);
            localStorage.setItem('access_token', response.data.token)
            await loginStore.getUserInfo();

            await router.push({path: '/'})
        }

이 부분으로

Pinia로 작성한 loginStore 모듈에 setToken과 getUserInfo를 호출할 것이고

결론적으로 사용자의 로컬스토리지에 'access_token'의 키값으로 토큰을 저장할 것이다.

 

로그인 전

 

 

로그인 후

 

현업에선 어떻게 사용하는지 모르지만 로컬스토리지에는 기본적으로 사용자의 정보가 담겨서는 안 된다고 한다.

그렇기에 토큰을 담아주고 이 토큰을 사용하여 사용자정보를 가져오는 방식으로 사용하려고 한다.

 


store/login.js

import {defineStore} from "pinia";
import {ref} from "vue";
import * as loginApi from '@/api/login/login'

export const useLoginStore = defineStore("login", () => {
    //state
    const token = ref(null);
    const userNo = ref(null);
    const username = ref(null);
    const role = ref(null);

    //actions
    async function setToken(newToken) {
        token.value = newToken;
    }

    //유저 정보
    async function getUserInfo() {
        try {
            const localToken = localStorage.getItem('access_token');
            const response = await loginApi.getUserInfo(localToken);

            token.value = localToken;
            userNo.value = response.data.data.userNo;
            username.value = response.data.data.username;
            role.value = response.data.data.role;
        } catch (e) {
            localStorage.removeItem('access_token');
        }
    }

    return {
        token,
        userNo,
        username,
        role,
        setToken,
        getUserInfo,
    }
})

 

Pinia의 사용방법에 빨리 익숙해지기 위해 사용하고 있다.

개인적으로 Vuex보다 훨씬 쉽게 사용할 수 있다고 생각하고 CompositionApi에 맞는 라이브러리가 아닌가 생각이 든다.

 

localStorage에서 'access_token' 값을 가져와 loginApi(토큰을 통해 유저정보 반환 Api)를 호출한다.

 

그 후 State값에 추가해 주면 된다.

 

※ catch의 경우

catch의 에러 값은 보통 '만료된 jwt' 혹은 '유효하지 않은 jwt'에러를 뱉는다.

그 이유로 에러를 뱉어준다면 localStorage에 있는 토큰을 지워주는 것으로 상태값을 초기화해준다고 생각하였다.(개인적으론 정답은 아닌 코드라고 생각한다.)

 


@/api/login.js

import http from "@/api/http";

/**
 * 로그인 api
 * @param username
 * @param password
 * @returns {Promise<axios.AxiosResponse<any>>}
 */
export async function login(username, password) {

    const loginDto = {
        username: username,
        password: password
    }
    return await http.post('/api/login', loginDto);
}

/**
 * 토큰을 사용해 유저 정보 api
 * @param token
 * @returns {Promise<void>}
 */
export async function getUserInfo(token) {
    return await http.get('/api/users', {
        headers: {
            Authorization: `Bearer ${token}`
        }
    });
}

axios 통신이다. 프런트엔드에서 토큰값을 전해줄 때 headers에다가 전달해 주는 것을 잊지 않아야 한다.

 


App.vue

<template>
    <v-app>
        <NavBar/>
        <AppBar/>
        <v-main>
            <v-container>
                <router-view/>
            </v-container>
        </v-main>
    </v-app>
</template>

<script setup>
import NavBar from "@/components/NavBar.vue";
import AppBar from "@/components/AppBar.vue";
import {useLoginStore} from "@/store/login";

const loginStore = useLoginStore();

loginStore.getUserInfo();

</script>

<style scoped>
.v-application {
    background-color: lightgray;
}
</style>

마지막으로 App.vue에 pinia에서 만든 getUserInfo를 사용하여 토큰값이 존재할 경우 계속해서 사용자의 정보를 유지하는 방식을 만들었다.

 

이것으로 새로고침이나 state가 초기화되는 상황에서 만약 사용자의 localStorage에 토큰값이 있다면 유저정보를 가져와 자동으로 로그인할 수 있는 방식으로 만들 수 있게 되었다.

'Frontend > Vue.js' 카테고리의 다른 글

[Vue.js3] Vuetify Validation : Rules  (0) 2023.07.01
[Vue.js3] <script> or <script setup>  (0) 2023.06.28
[Vue.js3] Pinia 란?  (0) 2023.06.19
[Vue.js3] Vuex란  (0) 2023.05.19
Vue.js3 구조 이해  (0) 2023.04.21
Comments