[Vue] DRF Auth with Vue (2) - front server

문지은·2023년 6월 18일
0

Vue

목록 보기
15/15
post-thumbnail

Vue Server 요청 정상 작동 여부 확인

  • 정상 작동하던 게시글 전체 조회 요청이 작동하지 않음
    • 401 status code 확인
    • 인증되지 않은 사용자이므로 조회 요청이 불가능해진 것
    • header 에 토큰을 넣어서 보내면 해결 할 수 있다!

SignUp Request

SignUp Page

  • Server에서 정의한 field명에 따라 view 작성
    1. username
    2. password1
    3. password2
// views/SignUpView.vue

<template>
  <div>
    <h1>Sign Up Page</h1>
    <form @submit.prevent="signUp">
      <label for="username">username : </label>
      <input type="text" id="username" v-model="username"><br>

      <label for="password1"> password : </label>
      <input type="password" id="password1" v-model="password1"><br>

      <label for="password2"> password confirmation : </label>
      <input type="password" id="password2" v-model="password2">
      
      <input type="submit" value="SignUp">
    </form>
  </div>
</template>
  • 라우터 등록
// router/index.js

...
import SignUpView from '@/views/SignUpView'
...

Vue.use(VueRouter)

const routes = [
  ...
  {
    path: '/signup',
    name: 'SignUpView',
    component: SignUpView
  },
]
  • 라우터 링크 작성
    • 파이프 라인 등을 활용하여 링크간 공간 확보
// src/App.vue

<template>
  <div id="app">
    <nav>
      <router-link :to="{ name: 'ArticleView' }">Articles</router-link> | 
      <router-link :to="{ name: 'SignUpView' }">SignUpPage</router-link>
    </nav>
    <router-view/>
  </div>
</template>
  • views/SignUpView.vue 결과 확인

SignUp Request

  • 회원가입을 완료 시 응답 받을 정보 Token을 store에서 관리할 수 있도록 actions를 활용하여 요청 후, state에 저장할 로직 작성
    • 회원가입이나 로그인 후 얻을 수 있는 Token은 server를 구성 방식에 따라 매 요청마다 요구 할 수 있으므로, 다양한 컴포넌트에서 쉽게 접근 할 수 있도록 중앙 상태 저장소인 vuex에서 관리
  • 사용자 입력 값을 하나의 객체 payload에 담아 전달
// views/SignUpView.vue

<script>
export default {
  name: 'SignUpView',
  data() {
			username: null,
      password1: null,
      password2: null,
  },
  methods: {
    signUp() {
      const username = this.username
      const password1 = this.password1
      const password2 = this.password2

      const payload = {
        username, password1, password2
      }
      this.$store.dispatch('signUp', payload)
    }
  }
}
</script>
  • payload가 가진 값을 각각 할당
    • AJAX 요청으로 응답 받은 데이터는 다수의 컴포넌트에서 사용해야 함
    • state에 저장 할 것
// store/index.js

actions: {
    signUp(context, payload) {
      const username = payload.username
      const password1 = payload.password1
      const password2 = payload.password2

      axios({
        method: 'post',
        url: `${API_URL}/accounts/signup/`,
        data: {
          username, password1, password2
        }
      })
        .then(res => {
          context.commit('SIGN_UP', res.data.key)
        })
        .catch(err => console.log(err))
    },
  • token을 저장할 위치 확인
    • mutations를 통해 state 변화
// store/index.js

export default new Vuex.Store({
  state: {
    token: null,
  },
  getters: {
  },
  mutations: {
    //auth
    SIGN_UP(state, token) {
      state.token = token
    },
  },
  • 요청 결과 확인
    • 정상 응답 확인

토큰 관리

  • 게시물 전체 조회와 달리, 인증 요청의 응답으로 받은 Token은 매번 요청하기 힘듦
    • 비밀번호를 항상 보관하고 있을 수는 없음
    • localStorage에 token 저장을 위해 vuex-persistedstate 활용
  • vuex-persistedstate 설치
$ npm install vuex-persistedstate
// store/index.js

import createPersistedState from 'vuex-persistedstate'

export default new Vuex.Store({
  plugins: [
    createPersistedState(),
  ],
	...
  • 결과 확인
    • localStorage에 저장

[참고] User 인증 정보를 localStorage에 저장해도 되는가?

  • 안전한 방법으로 볼 수는 없음
  • 따라서, vuex-persistedstate는 아래의 2가지 방법을 제공
    • 쿠키를 사용하여 관리
    • 로컬 저장소를 난독화 하여 관리
  • 실습의 편의를 위해 localStorage를 사용할 예정

LogIn Request

LogIn Page

  • Server에서 정의한 field명에 따라 view 작성
    1. username
    2. password
// views/LogInView.vue

<template>
  <div>
    <h1>LogIn Page</h1>
    <form @submit.prevent="login">
      <label for="username">username : </label>
      <input type="text" id="username" v-model="username"><br>

      <label for="password"> password : </label>
      <input type="password" id="password" v-model="password"><br>

      <input type="submit" value="logIn">
    </form>
  </div>
</template>
  • 라우터 등록 및 라우터 링크 작성
// router/index.js

...
import LogInView from '@/views/LogInView'
...

const routes = [
  ...
  {
    path: '/login',
    name: 'LogInView',
    component: LogInView
  },
]
// src/App.vue

<template>
  <div id="app">
    <nav>
      <router-link :to="{ name: 'ArticleView' }">Articles</router-link> | 
      <router-link :to="{ name: 'SignUpView' }">SignUpPage</router-link> | 
      <router-link :to="{ name: 'LogInView' }">LogInPage</router-link>
    </nav>
    <router-view/>
  </div>
</template>
  • 결과 확인

LogIn Request

  • signUp과 다른 점은 password1 password2 가 password로 바뀐 것 뿐
  • 요청을 보내고 응답을 받은 Token을 state에 저장하는 것 까지도 동일
    • mutations가 처리 해야 하는 업무가 동일
    • SIGN_UP mutations를 SAVE_TOKEN mutations로 대체 가능
  • 사용자 입력 값을 하나의 객체 payload에 담아 전달
// views/LogInView.vue

export default {
  name: 'LogInView',
  data() {
    return {
      username: null,
      password: null,
    }
  },
  methods: {
    login() {
      const username = this.username
      const password = this.password

      const payload = {
        username, password
      }

      this.$store.dispatch('login', payload)
    }
  }
}
  • payload가 가진 값을 각각 할당
    • AJAX 요청으로 응답 받은 데이터는 다수의 컴포넌트에서 사용해야 함
    • state에 저장 할 것
// store/index.js

actions: {
	...
	login(context, payload) {
      const username = payload.username
      const password = payload.password

      axios({
        method: 'post',
        url: `${API_URL}/accounts/login/`,
        data: {
          username, password
        }
      })
        .then((res) => {
          context.commit('SAVE_TOKEN', res.data.key)
        })
        .catch((err) => {
          console.log(err)
        })
    }
}
  • 이 때, mutations는 SAVE_TOKEN 호출 확인
    • signUp이 호출할 mutations도 함께 변경
// store/index.js

mutations: {
    ...
		//auth
    // SIGN_UP(state, token) {
    //   state.token = token
    // },
    SAVE_TOKEN(state, token) {
      state.token = token
    }
  },

actions: {
		...
    signUp(context, payload) {
      const username = payload.username
      const password1 = payload.password1
      const password2 = payload.password2

      axios({
        method: 'post',
        url: `${API_URL}/accounts/signup/`,
        data: {
          username, password1, password2
        }
      })
        .then(res => {
          console.log(res)
          context.commit('SAVE_TOKEN', res.data.key)
        })
        .catch(err => console.log(err))
    },
		...
  • 최종 결과 확인
    • 정확한 결과 확인을 위해 기존 토큰 삭제 추천

IsAuthenticated in Vue

  • 회원가입, 로그인 요청에 대한 처리 후 state에 저장된 Token을 직접 확인하기 전까지 인증 여부 확인 불가
  • 인증 되지 않았을 시 게시글 정보를 확인할 수 없으나 이유를 알 수 없음
    • 로그인 여부를 확인 할 수 있는 수단이 없음
  • 로그인 여부 판별 코드 작성
    • Token이 있으면 true 없으면 false 반환
// store/index.js

export default new Vuex.Store({
  ...
  getters: {
    isLogin(state) {
      return state.token ? true : false
    }
  },
})
  • isLogin 정보를 토대로 게시글 정보를 요청 할 것인지, LogInView로 이동시킬 것인지 결정
// views/ArticleView.vue

export default {
  name: 'ArticleView',
  components: {
    ArticleList,
  },
  computed:{
    isLogin() {
      return this.$store.getters.isLogin  // 로그인 여부
    }
  },
  created() {
    this.getArticles()
  },
  methods: {
    getArticles() {
      // 로그인이 되어 있으면 getArticles action 실행하고
      if (this.isLogin) {
        this.$store.dispatch('getArticles')
      } 
      // 로그인 되어 있지 않으면 login 페이지로 이동
      else {
        alert('로그인이 필요한 서비스 입니다.')
        this.$router.push({ name: 'LogInView' })
      }
    }
  }
}
  • store/index.js에서는 $router에 접근 할 수 없음
    • router를 import 해야 함
// store/index.js

import router from '../router'

export default new Vuex.Store({
  ...
  mutations: {
    //auth
    SIGN_UP(state, token) {
      state.token = token
			router.push({ name: 'ArticleView' })
    },
  },
  • 결과확인
    • localStorage에서 token 삭제 후, 새로 고침
    • Articles 링크 클릭 시 LogInPage로 이동
      • 인증 되지 않은 사용자를 LogInPage로 이동 시키는데 성공

  • 로그인 후, Articles에서는..
    • 인증은 받았지만 게시글 조회 시 인증 정보를 담아 보내고 있지 않음

  • 원인
    • 로그인은 했으나 Django에서는 로그인 한 것으로 인식하지 못함
    • 발급 받은 token을 요청으로 보내지 않았기 때문

Request with Token

💡 이제 인증 여부를 확인하기 위한 Token 이 준비되었으니, headers HTTP에 Token을 담아 요청을 보내면 된다!

Article List Read with Token

  • getArticles() headers에 Authorizations 와 token 추가
// store/index.js

actions: {
    getArticles(context) {
      axios({
        method: 'get',
        url: `${API_URL}/api/v1/articles/`,
        headers: {
          Authorization: `Token ${ context.state.token }`
        }
      })
        .then((res) => {
        // console.log(res, context)
          context.commit('GET_ARTICLES', res.data)
        })
        .catch((err) => {
        console.log(err)
      })
    },
  • 결과 확인
    • 404 발생 원인은 view 함수가 그렇게 처리하기로 하였기 때문
    • 게시글 생성 기능 완성 후, 다시 결과 확인

Article Create with Token

  • createArticle() headers에 Authorization 와 token 추가
// views/CreateView.vue

export default {
  ...
  methods: {
    createArticle() {
      ...
      axios({
        method: 'post',
        url: `${API_URL}/api/v1/articles/`,
        data: { title, content},
        headers: {
          Authorization: `Token ${ this.$store.state.token }`
        },
      })
      .then(() => {
        // console.log(res)
        this.$router.push({name: 'ArticleView'})
      })
      .catch((err) => {
        console.log(err)
      })
    }
  }
}
  • 결과 확인

Create Article with User

  • 게시글을 작성시 User 정보를 포함하여 작성하도록 수정
    • User정보를 Vue에서도 확인 가능하도록 정보 제공
# articles/models.py

from django.db import models
from django.conf import settings

# Create your models here.
class Article(models.Model):
    # User정보를 Vue에서도 확인 가능하도록 정보 제공
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
  • makemigrations & migrate
    • 기존 게시글에 대한 User 정보 default 값 설정

  • ArticleListSerializer에서 user는 사용자가 작성 하지 않음 → fields에 추가
    • ArticleSerializer에서 user는 읽기 전용으로 제공
    • username을 확인 할 수 있도록 username field 정의 필요
      • comment_count field 정의 방법 참고
# articles/serializers.py

class ArticleListSerializer(serializers.ModelSerializer):
    username = serializers.CharField(source='user.username', read_only=True)

    class Meta:
        model = Article
        fields = ('id', 'title', 'content', 'user', 'username')

class ArticleSerializer(serializers.ModelSerializer):
    comment_set = CommentSerializer(many=True, read_only=True)
    comment_count = serializers.IntegerField(source='comment_set.count', read_only=True)
    username = serializers.CharField(source='user.username', read_only=True)

    class Meta:
        model = Article
        fields = '__all__'
        read_only_fields = ('user', )
  • 게시글 생성시 user 정보 저장
# articles/views.py

@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def article_list(request):
    if request.method == 'GET':
        # articles = Article.objects.all()
        articles = get_list_or_404(Article)
        serializer = ArticleListSerializer(articles, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            # serializer.save()
            serializer.save(user=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
  • article이 가지고 있을 user 정보 표현
// components/ArticleListItem.vue

<template>
  <div>
    <h5>{{ article.id }}</h5>
    <p>{{ article.title }}</p>
    <p>작성자 : {{ article.username }}</p>
    <router-link :to="{
      name: 'DetailView',
      params: {id: article.id }}">
      [DETAIL]
    </router-link>
    <hr>
  </div>
</template>
  • 결과 확인
    • 작성자 정보 확인 가능

프로젝트 전체 코드 확인

https://github.com/mjieun0956/TIL/tree/master/Vue/14.%20DRF%20Auth%20with%20Vue/front-server

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

2개의 댓글

comment-user-thumbnail
2023년 6월 23일

고수

1개의 답글