티스토리 뷰

Web/Front - log_in

로그인

잉_민 2022. 3. 23. 13:23

로그인/아웃 처리는 jwt를 발행/폐기 하는 방식으로 처리 한다.
발급된 토큰정보는 웹브라우저의 localStorage에 저장한다.
백엔드 서버에 전송할때 토큰은 request.headers.token에 전송 한다. (백엔드의 API를 확인할 것)
응답 받을때 토큰은 response.headers.token에 전송 받는다. (백엔드의 API를 확인할 것)

참고 우리는 별도의 헤더 정보를 사용하며 Bearer를 사용하지 않는다.

로그인 화면

로그인 화면은 상단의 app-header부분 없이 독립된 디자인의 페이지로 생성하도록 한다.

1) 로그인용 index.vue 파일 생성

라우터를 처리할 index.vue파일을 생성 한다.

/src/views/auth/index.vue

<template>
  <router-view />
</template>

2) 로그인 페이지 생성

로그인용 빈페이지를 하나 생성 한다.

/src/views/auth/login.vue

<template>
  <div>
    <h1>로그인</h1>
  </div>
</template>

<script>
export default {}
</script>

3) 라우터 등록

로그인 라우터는 path: '/'와 path: '*'사이에 (같은 동급으로)생성하도록 한다.
(path는 /auth를 사용 하자.)

/src/router/index.js

...
const routes = [
  {
    path: '/',
    component: () => import('../views'),
    redirect: '/home',
    children: [
      ...
    ]
  },
  {
    path: '/auth',
    component: () => import('../views/auth'),
    children: [
      {
        path: '/auth/login',
        component: () => import('../views/auth/login'),
        meta: { header: false }
      }
    ]
  },
  {
    path: '*',
    component: () => import('../components/NotFound.vue')
  }
]
...

로그인 주소는 /auth/login으로 한다.

5) 로그인 화면 생성

위에서 만든 로그인 페이지를 아이디/패스워드를 입력받는 화면으로 만들어 보자. (기능 제외)

/src/views/auth/login.vue

<template>
  <div>
    <div style="margin-top: 80px">
      <b-row align-h="center">
        <b-col cols="4">
          <b-card title="로그인">
            <b-form-group label-cols="4" label-cols-lg="3" label="아이디" label-for="input-userid">
              <b-form-input id="input-userid" v-model="userid"></b-form-input>
            </b-form-group>
            <b-form-group label-cols="4" label-cols-lg="3" label="패스워드" label-for="input-password">
              <b-form-input id="input-password" v-model="password" type="password"></b-form-input>
            </b-form-group>
            <b-form-group label-cols="4" label-cols-lg="3" label="">
              <b-button variant="primary" @click="onSubmit">로그인</b-button>
            </b-form-group>
          </b-card>
        </b-col>
      </b-row>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userid: null,
      password: null
    }
  },
  methods: {
    onSubmit() {
      console.log('onSubmit', this.userid, this.password)
    }
  }
}
</script>

토큰 발행

[로그인]버튼 클릭 시 동작시킬 Store파일을 만들어 보자.1) jwt-decode 설치

발급된 토큰의 정보를 확인하기 위해 jwt-decode를 설치 한다.

> npm install jwt-decode

2) auth 스토어 생성

토큰 처리를 위한 auth스토어를 생성 한다.

/src/store/models/auth.js

import api from '../apiUtil'
import jwtDecode from 'jwt-decode'

/*
  테스트용 토큰
  {
    "id": 1,
    "userid": "hong",
    "name": "홍길동",
    "role": "leader",
    "iat": 1639466985,
    "exp": 1954826985
  }
  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcmlkIjoiaG9uZyIsIm5hbWUiOiLtmY3quLjrj5kiLCJyb2xlIjoibGVhZGVyIiwiaWF0IjoxNjM5NDY2OTg1LCJleHAiOjE5NTQ4MjY5ODV9.-hTy681tbe62pV9tjArzc5Ig33frnh9j_NjegGiHJNw
*/
const testToken =
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcmlkIjoiaG9uZyIsIm5hbWUiOiLtmY3quLjrj5kiLCJyb2xlIjoibGVhZGVyIiwiaWF0IjoxNjM5NDY2OTg1LCJleHAiOjE5NTQ4MjY5ODV9.-hTy681tbe62pV9tjArzc5Ig33frnh9j_NjegGiHJNw'

const stateInit = {
  TokenUser: {
    id: null,
    userid: null,
    name: null,
    role: null,
    iat: null,
    exp: null
  }
}

export default {
  state: {
    TokenUser: { ...stateInit.TokenUser }, // token에서 추출한 사용자 정보
    Loading: false,
    Error: null
  },
  getters: {
    TokenUser: state => state.TokenUser,
    TokenLoading: state => state.Loading,
    TokenError: state => state.Error
  },
  mutations: {
    setTokenUser(state, data) {
      state.TokenUser = data
    },
    setLoading(state, data) {
      state.Loading = data
      state.Error = null
    },
    setError(state, data) {
      state.Error = data
      state.Loading = false
      state.TokenUser = { ...stateInit.TokenUser }
    },
    clearError(state) {
      state.Error = null
    }
  },
  actions: {
    authLogin(context, payload) {
      // 로그인 처리

      // 상태값 초기화
      context.commit('clearError')
      context.commit('setLoading', true)

      /* 테스트 데이터 세팅 */
      setTimeout(() => {
        const token = testToken
        const decodedToken = jwtDecode(token)

        // api를 호출하지 않으므로 직접 localStorage에 token을 저장 한다.
        window.localStorage.setItem('token', token)

        context.commit('setLoading', false)
        context.commit('setTokenUser', decodedToken)
      }, 2000) // 처리 시간을 2초로 주었다.

      /* RestApi 호출 */
      /*
      api
        .post('/serverApi/auths/token', payload)
        .then(response => {
          const token = response.headers.token
          const decodedToken = jwtDecode(token)

          // 정상인 경우 처리
          context.commit('setLoading', false)
          context.commit('setTokenUser', decodedToken)
        })
        .catch(error => {
          // 에러인 경우 처리
          context.commit('setLoading', false)
          context.commit('setError', error)
        })
      */
    }
  }
}

3) axios의 interceptor 활용

axios에서 제공하는 interceptors를 이용하여 request와 response할때 headers에 token값을 전송/수신 할 수 있다.
주의 토큰 정보를 헤더를 통해서 백엔드와 주고 받을 때에는 백엔드의 API와 맞춰야 한다.

/src/store/apiUtil.js

import axios from 'axios'

const api = axios.create()

// request(요청)시 아래의 로직이 인터셉트 된다.
api.interceptors.request.use(
  async request => {
    // header.token 전송
    const token = window.localStorage.getItem('token')
    request.headers.token = token

    return request
  },
  async error => {
    return Promise.reject(error)
  }
)

// response(응답)시 아래의 로직이 인터셉트 된다.### 응답 처리 여기서 먼제 해줌 !!
api.interceptors.response.use(
  async response => {
    // header.token 자동 갱신
    const token = response.headers.token // token을 header에서 받은 경우
    if (token) {
      window.localStorage.setItem('token', token)
      
      return response
    }
  },
  async error => {
    return Promise.reject(error)
  }
)

export default api

api.interceptors를 잘 눈여겨 보도록 하자.

4) store index.vue 등록

store의 index.vue에 등록한다. (Auth등록은 맨 위에 하자)

/src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import Auth from './models/auth'
import Department from './models/department'
import User from './models/user'
import Device from './models/device'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    Auth,
    Department,
    User,
    Device
  }
})

5) 로그인 페이지 완성

로그인 페이지를 완성해 보자.

/src/views/auth/login.vue

<template>
  <div>
    <div style="margin-top: 80px">
      <b-row align-h="center">
        <b-col cols="4">
          <b-card title="로그인">
            <b-form-group label-cols="4" label-cols-lg="3" label="아이디" label-for="input-userid">
              <b-form-input id="input-userid" v-model="userid"></b-form-input>
            </b-form-group>
            <b-form-group label-cols="4" label-cols-lg="3" label="패스워드" label-for="input-password">
              <b-form-input id="input-password" v-model="password" type="password"></b-form-input>
            </b-form-group>
            <b-form-group label-cols="4" label-cols-lg="3" label="">
              <b-button variant="primary" :disabled="loading" @click="onSubmit"
                ><b-spinner v-if="loading" small></b-spinner> 로그인</b-button
              >
            </b-form-group>
          </b-card>
        </b-col>
      </b-row>
    </div>
  </div>
</template>

<script>
import jwtDecode from 'jwt-decode'

export default {
  data() {
    return {
      userid: null,
      password: null
    }
  },
  computed: {
    tokenUser() {
      return this.$store.getters.TokenUser
    },
    loading() {
      return this.$store.getters.TokenLoading
    },
    error() {
      return this.$store.getters.TokenError
    }
  },
  watch: {
    tokenUser(value) {
      if (value && value.id && value.id > 0) {
        // 로그인이 완료된 상황
        this.$router.push('/home') // 메인 페이지 이동
      }
    },
    error(errValue) {
      if (errValue !== null) {
        // 메세지 출력
        this.$bvToast.toast('아이디/비밀번호를 확인해 주세요.', {
          title: '로그인 에러',
          variant: 'danger',
          solid: true
        })
      }
    }
  },
  created() {
    // 이미 토큰을 가지고 있는 경우 처리를 위한 로직
    const token = window.localStorage.getItem('token')
    if (token) {
      const decodedToken = jwtDecode(token)
      const today = new Date()
      const expDate = new Date(decodedToken.exp * 1000)

      if (expDate && expDate >= today) {
        // 토큰이 유효한 경우
        this.$router.push('/home') // 메인 페이지 이동
      } else {
        // 토큰이 만료된 경우
        window.localStorage.removeItem('token') // 토큰 삭제
      }
    }
  },
  methods: {
    onSubmit() {
      this.$store.dispatch('authLogin', { userid: this.userid, password: this.password })
    }
  }
}
</script>

[로그인]버튼에 loading 기능이 구현 되었다. (로그인 하는 동안 버튼이 spin됨)
주의 로그인이 실패한 경우 아이디나 비밀번호중 어느 것이 잘못되었는지 알려주지 않도록 한다. (보안에 유의)

로그인 테스트

  • 로그인 페이지(/auth/login) 접속 확인
  • 로그인 후 /home페이지로 이동하는지 확인
  • 로그인 후 localStorage에 token이 저장되는지 확인
    • 웹브라우저의 개발자도구에서 Application > Storage > Local Storage에서 찾을 수 있다.
    • 해당 토큰을 삭제하면 로그아웃이 된걸로 간주 된다.
  • 토큰을 보유한 상태에서 로그인 페이지(/auth/login)로 접속 시 메인 페이지(/home)로 리다이렉트 되는지 확인
  • 만료된 토큰을 보유한 상태에서 로그인 페이지(/auth/login)로 접속 시 로그인 페이지가 유지되는지 확인
    • 메인 페이지/home로 가면 안됨
  • 로그인 상태에서 페이지 reload를 해보자.
    • 토큰정보는 localStorage에 남아있는데 Store에 저장된 TokenUser정보는 없어진 것을 확인할 수 있다.
    • 이 문제는 토큰검사에서 해결하도록 한다.

'Web > Front - log_in' 카테고리의 다른 글

Vue_axios  (0) 2022.03.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함