DjangoとVue.jsで作るWEBアプリ(その2: 認証機能編)

2019-07-19Python,TIPS,Vue.jsDjango,Vuetify

前回の記事ではDjangoとVueを使用して新しいプロジェクトをセットアップしました。
今回の投稿ではまず認証機能について実装していきます。
まずはDjango側のバックエンドから始めていきます。

バックエンドの実装

まずはserverルートへ移動しマイグレートをします。

$ cd hedgehog/server
$ source path/to/python/activate

$ python manage.py migrate

その後スーパーユーザを作成しましょう。

(env)$ python manage.py createsuperuser
Username (leave blank to use 'hoge'): 
Email address: 
Password: 
Password (again): 
Superuser created successfully.

ユーザーを作成できたらお気に入りのエディタでプロジェクトディレクトリを開いてください。
私はVSCodeを愛用しています。
現時点でのserverフォルダの構造はこうなっています。

まずはDjango CORS hedderとDjango REST framework、hedgehogアプリを設定していきます。

settings.py

import datetime # 追加

...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework', # 追加
    'rest_framework.authtoken', # 追加
    'hedgehog', # 追加
]

REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

JWT_AUTH = {
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=86400),
    'JWT_ALLOW_REFRESH': True,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
}

CORS_ORIGIN_WHITELIST = (
    'http://localhost:8080',
)

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware', # 追加
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

INSTALLED_APPS への追加をした後 REST_FRAMEWORK, JWT_AUTH, CORS_ORIGIN_WHITELIST の3種を追記します。
MIDDLEWARE への追加も忘れないようにしてください。

'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=86400)
この部分でセッションの保存時間を24時間に設定しています。 今回はテストのため24時間としましたが15分や30分などもっと短い期限を設定することが多いと思います。 お好みの時間を指定してください。

CORS_ORIGIN_WHITELIST
この部分でAPIを使用できるソースを定義しています。
localhostとlocalhost:8080を追加していますが適宜変更してください。

それではもう一度マイグレートします。

今回の設計ではhedgehogアプリで全て構築する予定です。
よってまず最初に Modelの定義とAPIエンドポイントをルーティングします。

hedgehog/models.py

from django.db import models

# Create your models here.


class PileColor(models.Model):
    """Holds the Pile Color, like SALT & PEPPER, Cinnamon"""
    name = models.CharField(max_length=50)
    description = models.CharField(max_length=250)

    def __str__(self):
        return self.name


class HedgeHog(models.Model):
    """ The Model to hold a list of Hedgehogs """
    name = models.CharField(max_length=250)
    pile_color = models.ForeignKey('PileColor', on_delete=models.CASCADE)
    stars = models.FloatField(default=1.0)
    description = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name


class Comment(models.Model):
    """Stores Comments by users about the HedgeHog """
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    hedgehog = models.ForeignKey('HedgeHog', on_delete=models.CASCADE)
    comment = models.TextField()
    visible = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)

Modelsを変更したのでマイグレートします。

% python manage.py makemigrations
Migrations for 'hedgehog':
  hedgehog/migrations/0001_initial.py
    - Create model PileColor
    - Create model HedgeHog
    - Create model Comment

% python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, authtoken, contenttypes, hedgehog, sessions
Running migrations:
  Applying hedgehog.0001_initial... OK

それでは次にルーティングを追加しましょう。

urls.py

from django.contrib import admin
from django.urls import path

from rest_framework_jwt.views import obtain_jwt_token # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', obtain_jwt_token), # 追加
]

path('auth/', obtain_jwt_token)
このエンドポイントはユーザー名とパスワードを受け取って認証用のトークンを返してくれます。
そのためにperform_jwt_tokenのviewsをインポートしています。

早速 http://localhost:8000/auth/ にアクセスしてユーザー名とパスワードを入力してPOSTしましょう。
生成されたトークンが返ってくるのがわかると思います。
このトークンを使用してフロントエンド側でユーザーの身元を確認します。

入力するユーザー名とパスワードはcreatesuperuserで設定した値を使用して下さい。

それでは続いてクライアント側を作成しましょう。

フロントエンドの実装

まず現時点でのclientフォルダの構造は以下の様になっています。

今回はsrcフォルダを扱います。
まずsrc/App.vueを開いてデフォルトで記述されているコードを全て削除します。
その後以下に書き換えましょう。

src/App.vue

<template>
  <v-app>
   <router-view/>
  </v-app>
</template>

<script>
export default {
  name: 'App'
}
</script>

次にAuthとHedgeHogsのページを作成します。
Authページではユーザーの認証を行い、HedgeHogsページではユーザーがお気に入りのハリネズミにコメントできるようにします。

まずsrcフォルダの中にあるcomponentsフォルダを開きましょう、HelloWorld.vue が入っています。
ここに pages というフォルダを作成してください。
そして pages の中に Auth.vueHedgeHogs.vue を作成します。

最後にcomponentsフォルダの中にもう2つフォルダを作成します。
1つは comps という名前をつけて下さい、この中にはメニュー及びHedgeHogのリストにコンポーネントのリストなどを格納します。
もう1つは forms という名前をつけます、ここにはその名の通りフォームを格納します。

最終的にsrcフォルダの構造は以下の様になります。

早速まずは先ほど作成した新しい2つのページを登録しましょう。

src/router.js

import Vue from 'vue'
import Router from 'vue-router'

import Auth from '@/components/pages/Auth'
import HedgeHogs from '@/components/pages/HedgeHogs'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HedgeHogs',
      component: HedgeHogs
    },
    {
      path: '/auth',
      name: 'Auth',
      component: Auth
    }
  ]
})

Djangoのurls.pyでのルーティングに似ていますね。
それではAuth.vueを以下のように修正します。

src/components/pages/Auth.vue

<template>
    <v-container grid-list-md>
        <v-layout row wrap align-center justify-center fill-height>
            <v-flex xs12 sm8 lg4 md5>
                <v-card class="login-card">
                    <v-card-title>
                    <span class="headline">Login to HedgeHogs</span>
                    </v-card-title>

                    <v-spacer/>

                    <v-card-text>

                    <v-layout
                        row
                        fill-height
                        justify-center
                        align-center
                        v-if="loading"
                    >
                        <v-progress-circular
                        :size="50"
                        color="primary"
                        indeterminate
                        />
                    </v-layout>


                    <v-form v-else ref="form" v-model="valid" lazy-validation>
                        <v-container>

                        <v-text-field
                            v-model="credentials.username"
                            :counter="70"
                            label="Eメールアドレス"
                            maxlength="70"
                            required
                        />

                        <v-text-field
                            type="password"
                            v-model="credentials.password"
                            :counter="20"
                            label="パスワード"
                            maxlength="20"
                            required
                        />

                        </v-container>
                        <v-btn class="pink white--text" :disabled="!valid" @click="login">Login</v-btn>

                    </v-form>


                    </v-card-text>
                </v-card>
            </v-flex>
        </v-layout>
    </v-container>
</template>

<script>
export default {
    name: 'Auth',
    data: () => ({
        credentials: {},
        valid:true,
        loading:false
    }),
    methods: {
        login() {

        }
    }
}
</script>

v-container のようなコンポーネントはマテリアルデザインでインターフェースを簡単に実装できる便利な子達です。
続いてHedgeHogsについても修正していきます。

src/components/pages/HedgeHogs.vue

<template>
    <div>
        HedgeHog Page
    </div>
</template>

<script>
import router from "../../router";
export default {
  name: "HedgeHogs",
  mounted() {
    this.checkLoggedIn();
  },
  methods: {
    checkLoggedIn() {
        router.push('/auth');
    }
  }
};
</script>

認証ページにリダイレクトする checkLoggedIn() というメソッドを使用していますね。
これは後ほどコメント部分を実装する際にユーザーがログインしているかどうかを確認するために使用します。
ユーザーがログインしていない場合は認証ページにリダイレクトしてくれます。

それでは一度プロジェクトを実行してみましょう。

いい感じですね!
BootStrapのようにお手軽にUIを設計することができました。

さて、Auth.vueのコードをチェックしてみましょう。
<v-btn class="pink white--text" :disabled="!valid" @click="login">Login</v-btn>
Loginボタンがクリックされた時に login という関数が呼んでいますね。

この関数は単純に認証を行なってくれるものでログインを有効にしてくれます。
先ほどDjango側でトークンが発行されるのを確認できたと思いますがこのトークンはユーザーがログインするたびユーザーを認証するのに使われます。

この部分で入力値をバリデーションするための定義をしています。
実際にバリデーションをする部分はこの後実装していきます。

<v-text-field
    v-model="credentials.username"
    :counter="70"
    label="Eメールアドレス"
    maxlength="70"
    required
/>

<v-text-field
    type="password"
    v-model="credentials.password"
    :counter="20"
    label="パスワード"
    maxlength="20"
    required
/>

それではvue-sessionを登録するために main.js を修正します。

src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import VueSession from 'vue-session'

Vue.use(Vuetify)
Vue.use(VueSession)

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

次にAuth.vueにログインロジックを追加します。

src/components/pages/Auth.vue

<template>
    <v-container grid-list-md>
        <v-layout row wrap align-center justify-center fill-height>
            <v-flex xs12 sm8 lg4 md5>
                <v-card class="login-card">
                    <v-card-title>
                    <span class="headline">Login to HedgeHogs</span>
                    </v-card-title>

                    <v-spacer/>

                    <v-card-text>

                    <v-layout
                        row
                        fill-height
                        justify-center
                        align-center
                        v-if="loading"
                    >
                        <v-progress-circular
                        :size="50"
                        color="primary"
                        indeterminate
                        />
                    </v-layout>


                    <v-form v-else ref="form" v-model="valid" lazy-validation>
                        <v-container>

                        <v-text-field
                            v-model="credentials.username"
                            :counter="70"
                            label="ユーザー名"
                            :rules="rules.username"
                            maxlength="70"
                            required
                        />

                        <v-text-field
                            type="password"
                            v-model="credentials.password"
                            :counter="20"
                            label="パスワード"
                            :rules="rules.password"
                            maxlength="20"
                            required
                        />

                        </v-container>
                        <v-btn class="pink white--text" :disabled="!valid" @click="login">Login</v-btn>

                    </v-form>


                    </v-card-text>
                </v-card>
            </v-flex>
        </v-layout>
    </v-container>
</template>

<script>
import axios from 'axios';
import Swal from 'sweetalert2';
import router from '../../router';
export default {
    name: 'Auth',
    data: () => ({
        credentials: {},
        valid:true,
        loading:false,
        rules: {
        username: [
            v => !!v || "ユーザー名は必須です",
            v => (v && v.length > 4) || "ユーザー名は5文字以上でなければなりません",
            v => /^[a-z0-9_]+$/.test(v) || "許可されていない文字が入力されています"
        ],
        password: [
            v => !!v || "パスワードは必須です",
            v => (v && v.length > 4) || "ユーザー名は5文字以上でなければなりません"
        ]
        }
    }),
    methods: {
        login() {
            if (this.$refs.form.validate()) {
            this.loading = true;
            axios.post('http://localhost:8000/auth/', this.credentials).then(res => {
                this.$session.start();
                this.$session.set('token', res.data.token);
                router.push('/');
            // eslint-disable-next-line
            }).catch(e => {
                this.loading = false;
                Swal.fire({
                type: 'warning',
                title: 'Error',
                text: 'ユーザー名もしくはパスワード、または両方が間違っています',
                showConfirmButton:false,
                showCloseButton:false,
                timer:3000
                })
            })
            }
        }
    }
}
</script>

これでLogin()を実装できました。


ただし、このままでは正しいユーザー名とパスワードを入力してもログインページにリダイレクトされてきてしまいます。 これはHedgeHogs.vueのcheckLoggedIn() が原因です。
よってHedgeHogs.vueも修正してやります。

src/components/pages/HedgeHogs.vue

<template>
    <div>
        HedgeHog Page
    </div>
</template>

<script>
import router from "../../router";
export default {
    name: "HedgeHogs",
    mounted() {
        this.checkLoggedIn();
    },
    methods: {
        checkLoggedIn() {
        this.$session.start();
        if (!this.$session.has("token")) {
            router.push("/auth");
        }
        }
    }
};
</script>

それでは最後にVueとDjangoをそれぞれ起動させて動作を確認してみましょう。

バッチリですね。
SmartAlert2のお陰でアラートもいい感じになりました。

まとめ

これで認証機能の完成です!
正しい認証情報でログインするとHedgeHog.vueが表示されます。
そして他のタブで localhost:8080/ へアクセスするとログインページにリダイレクトされるようになりました。

次回の投稿ではAPIエンドポイントを構築しcliant側で使用してみたいと思います。 GitHubへは一旦次回アップロードします。

2019-07-19Python,TIPS,Vue.jsDjango,Vuetify

Posted by Kenny