๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
โญ Personal_Study/Vue

DRF Auth with Vue

by ํฌ์ŠคํŠธ์‰์ดํฌ 2022. 11. 23.

DRF Auth with Vue

vue server ์š”์ฒญ ์ •์ƒ ์ž‘๋™ ์—ฌ๋ถ€ ํ™•์ธ

โœ” 401 status code
โœ” ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์ด๋ฏ€๋กœ ์กฐํšŒ ์š”์ฒญ์ด ๋ถˆ๊ฐ€๋Šฅ!

SignUp Request

SignUp Page

  1. views/SignUpView.vue
// 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>

<script>
export default {
  name: 'SignUpView',
  data() {
    return{
      username: null,
      password1: null,
      password2: null,
    }
  },
  methods: {
  }
}
</script>

โœ” Server์—์„œ ์ •์˜ํ•œ field๋ช… ํ™•์ธ

  1. username
  2. password1
  3. password2

 

  1. router/index.js
// router/index.js

import SignUpView from '@/views/SignUpView'

Vue.use(VueRouter)

const routes = [
  ...
  {
    path: '/signup',
    name: 'SignUpView',
    component: SignUpView
  },
  ...
]
  1. src/App.vue
// 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>
  1. ๊ฒฐ๊ณผ ํ™•์ธ

SignUp Request

โœ” ํšŒ์›๊ฐ€์ž…์„ ์™„๋ฃŒ ์‹œ ์‘๋‹ต ๋ฐ›์„ ์ •๋ณด Token์„ store์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก actions๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์š”์ฒ˜ ํ›„, state์— ์ €์žฅํ•  ๋กœ์ง ์ž‘์„ฑ

  • ํšŒ์›๊ฐ€์ž…์ด๋‚˜ ๋กœ๊ทธ์ธ ํ›„ ์–ป์„ ์ˆ˜ ์žˆ๋Š” Toekn์„ server์˜ ๊ตฌ์„ฑ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋งค ์š”์ฒญ๋งˆ๋‹ค ์š”๊ตฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ์— ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ค‘์•™ ์ƒํƒœ ์ €์žฅ์†Œ์ธ vuex์—์„œ ๊ด€๋ฆฌ
  1. views/SignUpView.vue
// views/SignUpView.vue

<script>
export default {
  ...
  methods: {
    signUp() {
      const username = this.username
      const password1 = this.password1
      const password2 = this.password2

      const payload = {
        username: username,
        password1: password1,
        password2: password2,
      }

      this.$store.dispatch('signUp', payload)
    }
  }
}
</script>

โœ” ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ํ•˜๋‚˜์˜ ๊ฐ์ฒด payload์— ๋‹ด์•„ ์ „๋‹ฌ

  1. store/index.js
// store/index.js

actions: {
    signUp(context, payload) {
      axios({
        method: 'post',
        url: `${API_URL}/accounts/signup/`,
        data: {
          // ์ถ•์•ฝํ˜• ํ‘œํ˜„
          // username,
          // password1,
          // password2,
          username: payload.username,
          password1: payload.password1,
          password2: payload.password2,
        }
      })
        .then((res) => {
          console.log(res)
          context.commit('SIGN_UP', res.data.key)
        })
        .catch((err) => {
          console.log(err)
        })
    }
  },

โœ” payload๊ฐ€ ๊ฐ€์ง„ ๊ฐ’ ๊ฐ๊ฐ ํ• ๋‹น
โœ” AJAX ์š”์ฒญ์œผ๋กœ ์‘๋‹ต ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์ˆ˜์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ
โœ” state์— ์ €์žฅ

  1. store/index.js
// store/index.js

export default new Vuex.Store({
  state: {
    articles: [],
    toekn: null,
  },
  getters: {
  },
  mutations: {
    GET_ARTICLES(state, articles) {
      state.articles=articles
    },
    SIGN_UP(state, token){
      state.token = token
    }
  },
  ...
})

โœ” token์„ ์ €์žฅํ•  ์œ„์น˜ ํ™•์ธ
โœ” mutations์„ ํ†ตํ•ด state ๋ณ€ํ™”

  1. ์š”์ฒญ ๊ฒฐ๊ณผ ํ™•์ธ

ํ† ํฐ ๊ด€๋ฆฌ

โœ” ๊ฒŒ์‹œ๋ฌผ ์ „์ฒด ์กฐํšŒ์™€ ๋‹ฌ๋ฆฌ, ์ธ์ฆ ์š”์ฒญ์˜ ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ Token์„ ๋งค๋ฒˆ ์š”์ฒญํ•˜๊ธฐ ํž˜๋“ค๋‹ค.
โœ” localStorage์— Token ์ €์žฅ์„ ์œ„ํ•ด vuex-persistedstate ํ™œ์šฉ

  1. ์„ค์น˜
$ npm install vuex-persistedstate
  1. store/index.js
// store/index.js

import createPersistedState from 'vuex-persistedstate'


export default new Vuex.Store({
  plugins: [
    createPersistedState(),
  ],
  ...
})
  1. ๊ฒฐ๊ณผ ํ™•์ธ

[์ฐธ๊ณ ] User ์ธ์ฆ ์ •๋ณด๋ฅผ localStorage์— ์ €์žฅํ•ด๋„ ๋˜๋Š”๊ฐ€?

โœ” ๋ณด์•ˆ์˜ ๊ด€์ ์—์„œ ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.
โœ” ๋”ฐ๋ผ์„œ vuex-persistedstate๋Š” ์•„๋ž˜ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•œ๋‹ค.

  1. ์ฟ ํ‚ค ์‚ฌ์šฉํ•˜์—ฌ ๊ด€๋ฆฌ
  2. ๋กœ์ปฌ ์ €์žฅ์†Œ๋ฅผ ๋‚œ๋…ํ™” ํ•˜์—ฌ ๊ด€๋ฆฌ

Login Request

Login Page

  1. views./LogInView.vue
// 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>

<script>
export default {
  name: 'LogInView',
  data() {
    return{
      username: null,
      password: null
    }
  },
  methods: {
  }
}
</script>
  1. router/index.js
// router/index.js

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


Vue.use(VueRouter)

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

  1. src/App.vue
// 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>
  1. ๊ฒฐ๊ณผ ํ™•์ธ

Login Request

โœ” signUp๊ณผ ๋กœ์ง์€ ๋™์ผํ•˜๋‹ค.
โœ” ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›์€ Token์„ state์— ์ €์žฅ

  • mutations๊ฐ€ ์ฒ˜๋ฆฌํ•  ์—…๋ฌด๊ฐ€ ๋™์ผ
  • SIGN_UP mutations๋ฅผ SAVE_TOKEN mutations๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ
  1. views/LogInView.vue
// views/LogInView.vue

<script>
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)
    }
  }
}
</script>
  1. store/index.js actions
// 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) => {
          console.log(res.data.key)
          context.commit('SAVE_TOKEN', res.data.key)
        })
        .catch((err) => {
          console.log(err)
        })
    }
  },
  1. store/index.js mutations
// store/index.js

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

โœ” signUp์ด ํ˜ธ์ถœํ•  mutations๋„ ํ•จ๊ป˜ ๋ณ€๊ฒฝ

  1. ๊ฒฐ๊ณผ ํ™•์ธ

isAuthenticated in Vue

โœ” ํšŒ์› ๊ฐ€์ž…, ๋กœ๊ทธ์ธ ์š”์ฒญ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ํ›„ state์— ์ €์žฅ๋œ Token์„ ์ง์ ‘ ํ™•์ธํ•˜๊ธฐ ์ „๊นŒ์ง€๋Š” ์ธ์ฆ ์—ฌ๋ถ€ ํ™•์ธ ๋ถˆ๊ฐ€
โœ” ์ธ์ฆ๋˜์ง€ ์•Š์•˜์„ ์‹œ ๊ฒŒ์‹œ๊ธ€ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์œผ๋‚˜ ์ด์œ ๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. -> ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋‹จ x

  1. store/index.js
// store/index.js

export default new Vuex.Store({
  ...
  getters: {
    isLogin(state) {
      return state.token ? true:false
    }
  },
  ...
})

โœ” ๋กœ๊ทธ์ธ ์—ฌ๋ถ€ ํŒ๋ณ„ ์ฝ”๋“œ ํ™•์ธ: Toekn์ด ์žˆ์œผ๋ฉด true, ์—†์œผ๋ฉด false ๋ฐ˜ํ™˜

  1. views/ArticleView.vue
// views/ArticleView.vue

<script>
import ArticleList from '@/components/ArticleList'

export default {
  ...
  methods: {
    getArticles() {
      if (this.isLogin) {
        this.$store.dispatch('getArticles')
      } else {
        alert('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.')
        this.$router.push({ name: 'LogInView'})
      }
    }
  }
}
</script>
  1. store/index.js
// store/index.js

import router from '@/router'

export default new Vuex.Store({
  ...
  mutations: {
    ...
    SAVE_TOKEN(state, token){
      state.token = token
      router.push({name:'ArticleView'})
    }
  },

โœ” router importํ•˜๊ธฐ!

  1. ๊ฒฐ๊ณผ ํ™•์ธ

โœ” localStorage์—์„œ token ์‚ญ์ œ ํ›„ ์ƒˆ๋กœ ๊ณ ์นจ
โœ” Articles ๋งํฌ ํด๋ฆญ ์‹œ LogInPage๋กœ ์ด๋™

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ...

โœ” ์ธ์ฆ์€ ๋ฐ›์•˜์ง€๋งŒ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ์‹œ ์ธ์ฆ ์ •๋ณด๋ฅผ ๋‹ด์•„์„œ ๋ณด๋‚ด๊ณ  ์žˆ์ง€ ์•Š๋‹ค!
โœ” ๋ฐœ๊ธ‰ ๋ฐ›์€ token์„ ์š”์ฒญ์œผ๋กœ ๋ณด๋‚ด์ง€ ์•Š์•„์„œ ๋กœ๊ทธ์ธ์€ ํ–ˆ์œผ๋‚˜ Django์—์„œ๋Š” ๋กœ๊ทธ์ธํ•œ ๊ฒƒ์œผ๋กœ ์ธ์‹ํ•˜์ง€ ๋ชปํ•œ๋‹ค.

Request with Token

โœ” ์ธ์ฆ ์—ฌ๋ถ€ ํ™•์ธ์„ ์œ„ํ•œ Token์ด ์ค€๋น„ ๋˜์—ˆ์œผ๋‹ˆ headers HTTP์— token์„ ๋‹ด์•„ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ๋œ๋‹ค!

Article List Read with Token

  1. store/index.js
// store/index.js

export default new Vuex.Store({
  ...
  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)
        })
    },
    ...
})

โœ” headers์— Authorizations์™€ token ์ถ”๊ฐ€

Article Create with Token

  1. views/CreateView.vue
// views/CreateView.vue
<script>
...

export default {
  ...
  methods: {
    createArticle() {
      ...
      axios({
        method: "post",
        url: `${API_URL}/api/v1/articles/`,
        headers: {
          Authorization: `Token ${ this.$store.state.token }`
        },
        data: {
          title,
          content,
        },
      })
        .then(() => {
          this.$router.push({ name: "ArticleView" });
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
};
</script>

Create Article with User

  1. articles/models.py
    # articles/models.py
    
    from django.db import models
    from django.conf import settings
    
    # Create your models here.
    class Article(models.Model):
        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)

โœ” ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์‹œ User ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์—ฌ ์ž‘์„ฑํ•˜๋„๋ก ์ˆ˜์ • -> User ์ •๋ณด๋ฅผ Vue์—์„œ๋„ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ •๋ณด ์ œ๊ณต  

  1. makemigrations, migrate

โœ” ๊ธฐ์กด ๊ฒŒ์‹œ๊ธ€์— ๋Œ€ํ•œ User์ •๋ณด defualt ๊ฐ’ ์„ค์ •

  1. articles/serializers.py
# articles/serializers.py

from rest_framework import serializers
from .models import Article, Comment


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

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


class CommentSerializer(serializers.ModelSerializer):

    class Meta:
        model = Comment
        fields = '__all__'
        read_only_fields = ('article',)


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', )

โœ” ArticleListSerializer์—์„œ user๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค -> fields์— ์ถ”๊ฐ€
โœ” ArticleSerializer์—์„œ user๋Š” ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์ œ๊ณต
โœ” username์„ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋„๋ก username field ์ •์˜ ํ•„์š”

  1. articles/views.py
# 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)
  1. components/ArticleListItem.vue
// compoents/ArticleListItem.vue

<template>
  <div>
    <h5>{{ article.id }}</h5>
    <p>์ž‘์„ฑ์ž: {{ article.username }}</p>
    <p>{{ article.title }}</p>
    ...
    <hr>
  </div>
</template>

'โญ Personal_Study > Vue' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

drf-spectacular  (0) 2022.11.24
DFR Auth System  (0) 2022.11.22
Vue with DRF  (0) 2022.11.21
Cross - Origin Resource Sharing (CORS)  (0) 2022.11.20
Vue with DRF: Server & Client  (0) 2022.11.20

๋Œ“๊ธ€