Nuxt.jsにnuxt/authを導入して、導入したDevise Token Auth向けにテスト(RSpec)を書くで作成したRailsアプリと連携して認証(ログイン・ログアウト)を実装してみました。
authとauth-nextの違い
@nuxtjs/auth-next (v5)
@nuxtjs/auth (v4)
バージョンの違いかな。安定版はv4っぽい。
auth-nextも試しましたが動かなかったので、今回はauthを採用。
nuxt/auth導入
Nuxt.jsでVue.jsに触れてみる で作成したアプリに導入します。
$ yarn add @nuxtjs/auth
package.jsonとyarn.lockが更新される。
nuxt.config.js に追加
modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', + // https://auth.nuxtjs.org/ + '@nuxtjs/auth', // https://go.nuxtjs.dev/pwa '@nuxtjs/pwa' ],
tsconfig.json に追加(今回は使わないけど、いずれTypeScriptに書き換えたいので入れておく)
"types": [ "@nuxt/types", "@nuxtjs/axios", + "@nuxtjs/auth", "@types/node" ]
store/index.js を作成
※空ファイルで良い
nuxt/authの設定
nuxt.config.js に追加
export default { + server: { + port: 5000 + },
ポートがRailsアプリと被る。先にRailsを起動して、Nuxt動かせばポート変わるけど、逆だとエラーになるし、RailsのURLを設定するので、序でに変更しておく。
auth: { redirect: { login: '/login', logout: '/login', callback: false, home: '/' }, strategies: { local: { token: { property: 'token', global: true }, user: { property: 'user' }, endpoints: { login: { url: 'http://localhost:3000/users/auth/sign_in.json', method: 'post' }, logout: { url: 'http://localhost:3000/users/auth/sign_out.json', method: 'delete' }, user: { url: 'http://localhost:3000/users/auth/validate_token.json', method: 'get' } } } } },
endpointsは、RailsアプリのDevise Token AuthのURLを設定
ログインページ作成
pages/login.vue を作成
<template> <v-card max-width="480px"> <v-form> <v-card-title> ログイン </v-card-title> <v-card-text> <v-text-field v-model="email" label="メールアドレス" /> <v-text-field v-model="password" type="password" label="パスワード" /> <v-btn color="primary" @click="login"> ログイン </v-btn> </v-card-text> </v-form> </v-card> </template> <script> export default { middleware: 'auth', // TODO: トップページでalert表示「既にログインしています。」 data () { return { email: '', password: '' } }, methods: { async login () { await this.$auth.loginWith('local', { data: { email: this.email, password: this.password } }) .then((response) => { if (response.data.alert) { console.log('[OK]alert: ' + response.data.alert) } // TODO: 遷移元ページでalert表示 if (response.data.alert) { console.log('[OK]notice: ' + response.data.notice) } // TODO: 遷移元ページでnotice表示 return response }, (error) => { if (typeof error.response === 'undefined') { console.log('[ERROR]' + error) // TODO: alert表示 } else { if (error.response.data.alert) { console.log('[NG]alert: ' + error.response.data.alert) } // TODO: alert表示 if (error.response.data.notice) { console.log('[NG]notice: ' + error.response.data.notice) } // TODO: notice表示 } return error }) } } } </script>
通信エラーやレスポンスをメッセージ表示させたいけど、今はログに出力して、TODOにしておく。
→ 対応しました。コミットログ参照
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/8d6b7cb6c61470697cc9b5bc4a479aaa84e8b3a2
ログアウトページ作成
アクションだけでも良いですが、確認した方が間違えてログアウトするのを避けられそうなので、ページにしてみました。ネットバックとかで良く見かける。
pages/logout.vue を作成
<template> <v-card max-width="480px"> <v-card-title> ログアウトします。よろしいですか? </v-card-title> <v-card-text> <v-btn to="/" nuxt> トップページ </v-btn> <v-btn color="primary" @click="logout"> ログアウト </v-btn> </v-card-text> </v-card> </template> <script> export default { middleware: 'auth', // TODO: ログインページでnotice表示「既にログアウト済みです。」 methods: { logout () { this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」 } } } </script>
最初、ログインと同じようにレスポンスで制御しようとしましたが、responseがundefinedになりました。logoutWithもなさそう。
そもそも通信に失敗してもフロントはログアウト状態になるので、ハンドリングしても意味がない。
よくよく考えると、サーバーサイドのtoken消すよりも、ユーザーの状態変更を優先した方が良い。
いずれ有効期限切れるし、デフォルト最大10件なので、古いのから追い出されるし。
Devise Token Auth対応
Devise Token Authでは、ヘッダーのuid/client/access-tokenを認証に使うので、このままだとログイン状態を維持できない。→ Devise Token Authの挙動を確認してみた
sign_in.jsonの直後にvalidate_token.jsonが呼ばれ、ヘッダーにuid/client/access-tokenが無いのでtokenチェックに失敗する。
nuxt.config.js に追加
plugins: [ + { src: '~/plugins/axios.js' } ],
plugins/axios.js を作成
export default function ({ $axios }) { $axios.onRequest((config) => { // Devise Token Auth if (localStorage.getItem('token-type') === 'Bearer' && localStorage.getItem('access-token')) { config.headers.uid = localStorage.getItem('uid') config.headers.client = localStorage.getItem('client') config.headers['access-token'] = localStorage.getItem('access-token') } }) $axios.onResponse((response) => { // Devise Token Auth if (response.headers['token-type'] === 'Bearer' && response.headers['access-token']) { localStorage.setItem('token-type', response.headers['token-type']) localStorage.setItem('uid', response.headers.uid) localStorage.setItem('client', response.headers.client) localStorage.setItem('access-token', response.headers['access-token']) localStorage.setItem('expiry', response.headers.expiry) } }) }
Devise Token Auth以外では無視されるようにtoken-typeとaccess-tokenの有無でチェック。
一定時間内のリクエスト(batch_request)はaccess-tokenとexpiryに半角スペース(フロントでは空)になるので、onResponseではaccess-tokenがある場合のみローカルストレージに保存。
クリーニング
消さなくても動くけど、下記でログアウト時にローカルストレージから削除する処理を追加。
pages/logout.vue に追加
- logout () { + async logout () { - this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」 + await this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」 + // Devise Token Auth + if (localStorage.getItem('token-type') === 'Bearer' && localStorage.getItem('access-token')) { + localStorage.removeItem('token-type') + localStorage.removeItem('uid') + localStorage.removeItem('client') + localStorage.removeItem('access-token') + localStorage.removeItem('expiry') } }
async/awaitにしないと、先にローカルストレージから削除され、sign_out.jsonが失敗します。
動作確認
ログインするとトップページに遷移します。
OPTIONS(preflight)リクエストも正常に返却されています。
こちら設定済み → CORS設定でOPTIONSリクエストとヘッダ取得できない問題に対応する
動線を作る
サイドメニューにログイン・ログアウトリンクを設置します。
layouts/default.vue を変更
<v-list> <v-list-item v-for="(item, i) in items" + v-if="(item.loggedIn == null) || (item.loggedIn == $auth.loggedIn)" :key="i" :to="item.to" router exact >
<script> export default { data () { return { clipped: false, drawer: false, fixed: false, items: [ { icon: 'mdi-apps', title: 'Welcome', - to: '/' + to: '/', + loggedIn: null }, { icon: 'mdi-chart-bubble', title: 'Inspire', - to: '/inspire' + to: '/inspire', + loggedIn: null + }, + { + icon: 'mdi-login', + title: 'ログイン', + to: '/login', + loggedIn: false + }, + { + icon: 'mdi-logout', + title: 'ログアウト', + to: '/logout', + loggedIn: true } ],
リファクタリング
これでも動くがlint掛けるとerrorになる。
$ yarn lint 13:11 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for
v-forとv-ifは一緒に使わないでと。優先度の問題らしい。
【Vue.js】`v-for`, `v-if` を一緒に使うのは避けよう – Qiita
メッセージにあるcomputed property(算出プロパティ)を使ってしました。
<v-list> <v-list-item - v-for="(item, i) in items" + v-for="(item, i) in displayItems" - v-if="(item.loggedIn == null) || (item.loggedIn == $auth.loggedIn)" :key="i" :to="item.to" router exact >
+ }, + + computed: { + displayItems () { + return this.items.filter(item => (item.loggedIn == null) || (item.loggedIn === this.$auth.loggedIn)) + } } } </script>
“Nuxt.jsとRailsアプリのDevise Token Authを連携させて認証する” に対して1件のコメントがあります。