Old Branch

Vue.js & Flask 로그인/로그아웃 (4) - Vue.js Component 연동하기

woolbro 2020. 8. 23. 10:46
반응형

이전 글 첨부

[Master_branch/develop_branch] - Vue.js & Flask - 개발환경구축하기

[Master_branch/develop_branch] - Vue.js & Flask - 컴포넌트와 api 연동(1) - backend

[Master_branch/develop_branch] - Vue.js & Flask - 컴포넌트와 api 연동(2) - frontend

[Master_branch/develop_branch] - Vue.js & Flask - 데이터 주고받기 (axios)

[Master_branch/develop_branch] - Vue.js & Flask - 데이터 주고받기 axios(2) button binding

[Master_branch/develop_branch] - Vue.js & Flask 로그인/로그아웃 (1) - Docker DB 세팅하기

[Master_branch/develop_branch] - Vue.js & Flask 로그인/로그아웃 (2) - Flask Auth API 만들기

[Master_branch/develop_branch] - Vue.js & Flask 로그인/로그아웃 (3) - Flask Auth Token 적용하기

 

JWT토큰을 발행하고 할당, 그리고 Validation까지 했으니, 이제 이 동작들을 Frontend로 가져가서 작업 해 보도록 하자

front-back의 소스는 여기 올려놓았다

JWT Auth In Frontend

structure

  • frontend 구조라 스트럭쳐가 꽤 길다

      .
      ├── CHANGELOG.md
      ├── ISSUES_TEMPLATE.md
      ├── LICENSE.md
      ├── README.md
      ├── babel.config.js
      ├── front.dev.Dockerfile
      ├── package.json
      ├── public
      │    ....
      │   ├── index.html
      │   ├── manifest.json
      │   └── robots.txt
      ├── src
      │   ├── App.vue
      │   ├── assets
      │   │ ...
      │   ├── components
      │   │   ├── Badge.vue
      ... 
      │   │   ├── Tweet
      │   │   │   └── Tweet.vue
      │   │   └── stringUtils.js
      │   ├── directives
      │   │   └── click-ouside.js
      │   ├── layout
      │   │   ├── AuthLayout.vue
      │   │   ├── Content.vue
      │   │   ├── ContentFooter.vue
      │   │   ├── DashboardLayout.vue
      │   │   └── DashboardNavbar.vue
      │   ├── main.js
      │   ├── plugins
      │   │   ├── argon-dashboard.js
      │   │   ├── globalComponents.js
      │   │   └── globalDirectives.js
      │   ├── registerServiceWorker.js
      │   ├── router.js
      │   ├── utils
      │   │   └── index.js
      │   └── views
      │       ├── Dashboard
      │       │   ├── PageVisitsTable.vue
      │       │   └── SocialTrafficTable.vue
      │       ├── Dashboard.vue
      │       ├── Icons.vue
      │       ├── Login.vue
      │       ├── Maps.vue
      │       ├── MyLittleDiv
      │       │   └── MyLittleDiv.vue
      │       ├── Register.vue
      │       ├── Tables
      │       │   └── ProjectsTable.vue
      │       ├── Tables.vue
      │       └── UserProfile.vue
      ├── vue.config.js
      └── yarn.lock

적용 할 로직

backend의 코드에서 필요한 정보를 넘겨주기 때문에 ,front의 로직을 구상하는 것이 중요하다

  1. 로그인
    로그인 후 네비게이션 바 위에 내 이름이 뜨도록
  2. 로그아웃
    로그아웃 후 네비게이션 바 위에 이름이 없어지도록
  3. 게시글 리스트
    미리 저장되어있는 게시글 리스트를 불러 올 때, token값을 통해 인증 할 수 있도록

공통 Token Valid 함수

//
// frontend/src/utils/index.js

import Vue from 'vue'

export const EventBus = new Vue()

export function isValidJwt() {

    let jwt = localStorage.token

    if (!jwt || jwt.split('.').length < 3) {
        return false
    }
    const data = JSON.parse(atob(jwt.split('.')[1]))
    const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch
    const now = new Date()
    return now < exp
}

export default isValidJwt;

로그인

  • ID,PW 입력 → 로그인 요청 → backend → 토큰발행 → 토큰을 front로
  • 응답 받은 토큰을 가지고 frontend의 여러 곳에서 적용 할 수 있도록 공통모듈함수 생성

Login.vue, DashboardNavbar.vue 변경

  1. frontend/src/views/Login.vue

     //frontend/src/views/Login.vue - script 부분
     <script>
         import axios from 'axios';
    
         export default {
             name: 'login',
             data() {
                 return {
                     userInfo: {
                         username: '',
                         userpwd: ''
                     }
                 }
             },
             methods: {
                 makeLogin() {
                     let path = "http://" + window.location.hostname + ":5000/api2/auth/login";
                     axios.post(path, {
                         username: this.userInfo.username,
                         userpwd: this.userInfo.userpwd
                     }, {withCredential: true}).then((res) => {
                         localStorage.token = res.data.token
                         this.$router.push("/")
                         // if(this.userInfo.username == )
                     }).catch((error) => {
                         console.log(error);
                     });
                 },
             },
         }
     </script>

    javascript에서 userInfo에 user 정보를 담고, methods에서 로그인을 도와줄 makeLogin()함수를 만든다.

    로그인이 완료 된 후에, backend에서 받은 토큰을 크롬 로컬 스토리지에 저장하고 Dashboard페이지로 돌아갈 수 있도록

    Vue%20js%20&%20Flask%20%E1%84%85%E1%85%A9%E1%84%80%E1%85%B3%E1%84%8B%E1%85%B5%E1%86%AB%20%E1%84%85%E1%85%A9%E1%84%80%E1%85%B3%E1%84%8B%E1%85%A1%E1%84%8B%E1%85%AE%E1%86%BA%20(4)%20-%20Vue%20js%20Comp%2043ebd0cdc13e4b4a940a88a9f5cd7228/Untitled.png

    이 모든 통신에 axios를 사용 할 것이기 때문에 설치 되어 있지 않다면 yarn, npm을 이용해서 설치 해 주어야 한다.

     //frontend/src/views/Login.vue - 위의 스크립트를 적용시킬 vue파일
     <template>
         <div class="row justify-content-center">
             <div class="col-lg-5 col-md-7">
                 <div class="card bg-secondary shadow border-0">
                     <div class="card-header bg-transparent pb-5">
                         <div class="text-muted text-center mt-2 mb-3"><small>Sign in with</small></div>
                         <div class="btn-wrapper text-center">
                             <a href="#" class="btn btn-neutral btn-icon">
                                 <span class="btn-inner--icon"><img src="img/icons/common/github.svg"></span>
                                 <span class="btn-inner--text">Github</span>
                             </a>
                             <a href="#" class="btn btn-neutral btn-icon">
                                 <span class="btn-inner--icon"><img  src="img/icons/common/google.svg"></span>
                                 <span class="btn-inner--text">Google</span>
                             </a>
                         </div>
                     </div>
                     <div class="card-body px-lg-5 py-lg-5">
                         <div class="text-center text-muted mb-4">
                             <small>Or sign in with credentials</small>
                         </div>
                         <form role="form">
                             <base-input class="input-group-alternative mb-3"
                                         placeholder="text"
                                         addon-left-icon="ni ni-email-83"
                                         v-model="userInfo.username">
                             </base-input>
    
                             <base-input class="input-group-alternative"
                                         placeholder="Password"
                                         type="password"
                                         addon-left-icon="ni ni-lock-circle-open"
                                         v-model="userInfo.userpwd">
                             </base-input>
    
                             <base-checkbox class="custom-control-alternative">
                                 <span class="text-muted">Remember me</span>
                             </base-checkbox>
                             <div class="text-center">
                                 <base-button type="primary" class="my-4" v-on:click="makeLogin">Sign in</base-button>
                             </div>
                         </form>
                     </div>
                 </div>
                 <div class="row mt-3">
                     <div class="col-6">
                         <a href="#" class="text-light"><small>Forgot password?</small></a>
                     </div>
                     <div class="col-6 text-right">
                         <router-link to="/register" class="text-light"><small>Create new account</small></router-link>
                     </div>
                 </div>
             </div>
         </div>
     </template>

    모두 기존 argon의 템플릿을 가져와서 작업 한 것이기 때문에 크게 바꾼 것은 없지만, 작업 한 것은 버튼과 데이터를 담는 공간을 설정 한 것이다.

    • Sign in 버튼에 v-on:click="makeLogin" 으로 버튼과 함수를 이어 주었다.
  2. front/src/layout/DashboardNavbar.vue 변경

     //front/src/layout/DashboardNavbar.vue - script 부분
     <script>
         import isValidJwt from '../utils'
    
         export default {
             data() {
                 return {
                     username: '',
                     activeNotifications: false,
                     showMenu: false,
                     searchQuery: ''
                 };
             },
             methods: {
                 makelogout() {
                     localStorage.removeItem('token');
                     location.reload();
                 },
                 isAuthenticated() {
                     if (isValidJwt()) {
                         let data = JSON.parse(atob(localStorage.token.split('.')[1]))
                         this.username = data.sub
                     }
                 },
                 toggleSidebar() {
                     this.$sidebar.displaySidebar(!this.$sidebar.showSidebar);
                 },
                 hideSidebar() {
                     this.$sidebar.displaySidebar(false);
                 },
                 toggleMenu() {
                     this.showMenu = !this.showMenu;
                 }
             },
             created() {
                 this.isAuthenticated()
             }
         };
     </script>

    이 스크립트에서 중요 한 것은, username을 새로 넣어주는 것이다. 간단한 axios 작업이기 때문에 큰 어려움은 없다.

     //front/src/layout/DashboardNavbar.vue - template부분
     <template>
         <base-nav class="navbar-top navbar-dark"
                   id="navbar-main"
                   :show-toggle-button="false"
                   expand>
             <form class="navbar-search navbar-search-dark form-inline mr-3 d-none d-md-flex ml-lg-auto">
                 <div class="form-group mb-0">
                     <base-input placeholder="Search"
                                 class="input-group-alternative"
                                 alternative=""
                                 addon-right-icon="fas fa-search">
                     </base-input>
                 </div>
             </form>
             <ul class="navbar-nav align-items-center d-none d-md-flex">
                 <li class="nav-item dropdown">
                     <base-dropdown class="nav-link pr-0">
                         <div class="media align-items-center" slot="title">
                     <span class="avatar avatar-sm rounded-circle">
                       <img alt="Image placeholder" src="img/theme/team-4-800x800.jpg">
                     </span>
                             <div class="media-body ml-2 d-none d-lg-block">
                                 <span class="mb-0 text-sm  font-weight-bold">{{ username }}</span>
                             </div>
                         </div>
    
                         <template>
                             <div class=" dropdown-header noti-title">
                                 <h6 class="text-overflow m-0">Welcome!</h6>
                             </div>
                             <router-link to="/profile" class="dropdown-item">
                                 <i class="ni ni-single-02"></i>
                                 <span>My profile</span>
                             </router-link>
                             <router-link to="/profile" class="dropdown-item">
                                 <i class="ni ni-settings-gear-65"></i>
                                 <span>Settings</span>
                             </router-link>
                             <router-link to="/profile" class="dropdown-item">
                                 <i class="ni ni-calendar-grid-58"></i>
                                 <span>Activity</span>
                             </router-link>
                             <router-link to="/profile" class="dropdown-item">
                                 <i class="ni ni-support-16"></i>
                                 <span>Support</span>
                             </router-link>
                             <div class="dropdown-divider"></div>
                             <a v-on:click="makelogout">
                                 <router-link to="/" class="dropdown-item">
                                     <i class="ni ni-user-run"></i>
                                     <span>Logout</span>
                                 </router-link>
                             </a>
                         </template>
                     </base-dropdown>
                 </li>
             </ul>
         </base-nav>
     </template>

로그아웃

  • 로그아웃은, 로컬스토리지에 저장된 토큰을 삭제 해 주고 refresh해 주는 형태로 작업했다.
  • 더 좋은 방법이 10000000% 있는데 나는 아직은 잘 모르겠다ㅜㅜ 도와주면 좋겠다 누가....
  • 위의 로그인 작업 하는 DashboardNavbar.vue 에서, 로그아웃 메뉴가 있어서 같이 작업했다.
makelogout() {
      localStorage.removeItem('token');
      location.reload();
  },
  • 다른 코드 없이 단지 Storage의 token 값을 비워주었다

Tweet(게시글 리스트)

backend에서 작성했었던 Tweet 리스트 조회에 @token_required 을 적용한 url에서는 token값이 인증 된 것이 아니라면 401 에러를 뱉어내고 조회하지 못한다.

Tweet는 컴포넌트를 하나 만들고, Dashboard.vue에 추가 해 주는 형태로 작성했다.

<template>
    <div class="Tweet">
        <div class="row" v-for="tweet in tweet_list" :key="tweet.id">
            <div class="col-xl-12 col-lg-12">
                <stats-card title="Total traffic"
                            type="gradient-red"
                            icon="ni ni-active-40"
                            class="mb-4 mb-xl-0"
                >
                    <span style="font-size: 30px">{{ tweet.words }}</span>
                    <template slot="footer">
                        <span class="text-success mr-2" style="font-size: 25px"><i class="fa fa-user"
                                                                                   aria-hidden="true"></i>  {{ tweet.creator }}</span>
                        <span class="text-nowrap">{{ tweet.created_at }}</span>
                    </template>
                </stats-card>
            </div>
        </div>
    </div>
</template>

<script>
    import axios from 'axios';

    export default {
        name: 'all-tweet',
        data() {
            return {
                tweet_list: [],
            }
        },
        methods: {
            getTweet() {
                let path = "http://" + window.location.hostname + ":5000/api2/board/tweet";
                let token = localStorage.token
                axios.defaults.headers.common['Authorization'] = `Bearer: ${token}`
                axios.get(path).then((res) => {
                    this.tweet_list = res.data;
                }).catch((error) => {
                    console.error(error);
                });
            }
        },
        created() {
            this.getTweet();
        }
    };
</script>
  • vue-for 문을 사용해서 여러개의 글을 출력 해 주었다
  • script의 axios에서는 다른 axios와 다르게 defaults.headers를 추가 해 주었다
  • 토큰 인증을 사용 하는 요청/응답 과정이기 때문에 적용 해 주었다.

어렵다뭔가...

기존에는 Django, flask, spring 각각의 프레임워크 내에서 front,back을 모두 작업 했었는데 이거를 나눠서 하려니까 뭔가 중간에 과정이 하나 더 추가 되니까 플로우가 생각 할 것이 많은 것 같다