Rails devise token authとNuxt.jsを連携(Twitter認証)
くこの続きです☺️
環境
・Rails 6.0.0(APIモード)
・devise token auth 1.1.3
・vue/cli 4.2.3
・Nuxt.js 2.4.2
ようやく管理サイトのクライアント側実装にとりかかれました🥺
前回API側のトークン認証機能を作ったのでそれをNuxt.jsと連携させます
管理サイト(自分しか使わない)なので土台を1から作るのは面倒なので、Nuxt.js + Vuetifyで作られている管理サイトテンプレートないか探してみました↓
デザインフレームワークにVuetifyを選んだのはコンポーネントが多いからでそれ以外とくに気にしなかったです
テンプレートの導入はReadmeに載ってる手順をそのまま実行していけばOKでした
ただあらかじめnode、npm、vue/cli、vue/cli-init入れる必要があります
npm run devで起動してみるとポートが3000で起動するようで、Railsのデフォルトポートとかぶってたのとlocalhostだけバインドしてたので設定変えました
# nuxt.js.config
...
module.exports = {
...
server: {
port: 8080,
host: '0.0.0.0'
},
...
アクセスするとよさげな管理サイトが立ち上がりました
本題のdevise_token_authとNuxt.jsを連携させていきます
以下がとても参考になりました
Nuxt.js側構築
まずトークン認証させるために必要な(便利な)モジュールをインストール
$ npm i --save @nuxtjs/axios
$ npm i --save cookie-universal-nuxt
有効化とaxios基本設定
# nuxt.config.js
module.exports = {
...
modules: [
'@nuxtjs/axios',
'cookie-universal-nuxt'
],
...
axios: {
host: 'localhost', // APIドメイン
port: 3000,
},
...
}
Vuexにユーザーと認証情報を持たせる
# store/index.js
export const state = () => ({
user: null,
auth: {},
drawer: true
})
export const mutations = {
user(state, value) {
state.user = value
},
auth(state, value) {
state.auth = value
},
toggleDrawer(state) {
state.drawer = !state.drawer
},
drawer(state, val) {
state.drawer = val
},
}
そのまんま、userにユーザー情報、authにトークン達を入れとく箱です。
axiosの共通処理を加える
# plugins/axios.js
export default ({ $axios, store, app }) => {
$axios.onRequest(config => {
const headers = store.state.auth
config.headers = headers
})
$axios.onResponse(response => {
if (response.headers['access-token']) {
const authHeaders = {
'access-token': response.headers['access-token'],
'client': response.headers['client'],
'expiry': response.headers['expiry'],
'uid': response.headers['uid']
}
store.commit('auth', authHeaders)
const session = app.$cookies.get('session')
if (session) {
session.tokens = authHeaders
app.$cookies.set('session', session, {
path: '/',
maxAge: 60 * 60 * 24 * 7
})
}
}
})
$axios.onError(error => {
return Promise.reject(error.response);
});
}
# nuxt.config.js
module.exports = {
...
plugins: [
...
'~/plugins/axios',
],
...
}
onRequestでリクエスト時にヘッダーにトークンを入れてあげます
onResponseでレスポンス時にトークンがあればVuexとCookieに保存してあげます(クッキーに入れる理由は後述)
ログイン時の処理
まず元々あるログインページがちょいと派手なので簡素(?)にします
# pages/login.vue
<template>
<v-app id="login" class="primary">
<v-content>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4 lg4>
<v-card class="elevation-1 pa-3">
<v-btn :href="twitterLoginURL">
Twitterログイン
</v-btn>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</v-app>
</template>
<script>
export default {
layout: 'default',
data: () => ({
twitter: {
url: 'http://localhost:3000/auth/twitter',
redirectUrl: 'http://localhost:8080/oauth/twitter/callback'
},
}),
computed: {
twitterLoginURL() {
return `${this.twitter.url}?auth_origin_url=${encodeURI(this.twitter.redirectUrl)}`
}
},
};
</script>
API側のTwitter認証URLに遷移するTwitterログインボタンを設置しているだけです。
こんな感じのリンク↓
http://[API側ドメイン]/auth/twitter?auth_origin_url=[リダイレクト先URL]
リダイレクト先にはクライアント側のコールバック用URLに飛ばしています
で、その飛ばし先のページを作ります
# pages/oauth/twitter/callback.vue
<template>
</template>
<script>
export default {
async fetch({ query, store, $axios, redirect, app }) {
const authHeaders = {
'access-token': query.auth_token,
'client': query.client_id,
'uid': query.uid,
'expiry': query.expiry,
}
store.commit('auth', authHeaders)
const { data } = await $axios.$get('/auth/validate_token')
store.commit('user', data)
const session = {
tokens: authHeaders,
user: data
}
app.$cookies.set('session', session, {
path: '/',
maxAge: 60 * 60 * 24 * 7
})
redirect(301, '/')
}
}
</script>
レンダリングする必要ないのでfetch内で処理しています
APIがここへコールバックする時にURLパラメータにトークンが入っているのでそれを取得してVuexに保存。
で、そのトークン使って/auth/validate_tokenでユーザー情報持ってきてVuexに保存。
で、トークンとユーザー情報をクッキーに入れてTOPページにリダイレクトしています。
クッキーに入れる理由はページリロードしちゃうとVuexが初期化されるので、保持しておくtemp的役割を担っています。
LocalStorageに入れてもできますが、セキュリティ的にまずいのでクッキーのがよいです。
ページリロード時にクッキーをVuexに入れる
# pulgins/cookie.vue
export default ({ store, app, $axios }) => {
const session = app.$cookies.get('session')
if (session) {
store.commit('user', session.user)
store.commit('auth', session.tokens)
}
}
# nuxt.config.js
module.exports = {
...
plugins: [
...
'~/plugins/cookie',
],
...
}
とてもシンプル
ログイン状態をわかるようにする
# components/AppToolbar.vue
<template>
...
<img :src="user.image" :alt="user.name"/>
...
</template>
<script>
export default {
...
computed: {
user() {
return this.$store.state.user || {}
},
...
},
...
}
</script>
ちょっと分かりづらいですが要はcomputedでvuexにあるuser情報持ってきて右上のアバターを動的にしてるだけです
未ログイン時のアクセス制御
# pulgins/redirect.js
export default function ({ store, route, redirect }) {
if (store.state.user) return Promise.resolve()
if (route.path === '/login') return Promise.resolve()
if (route.path.match(/^\/oauth\/.+\/callback$/)) return Promise.resolve()
window.location.href = '/login'
return new Promise((resolve) => {})
}
# nuxt.config.js
module.exports = {
...
plugins: [
...
'~/plugins/redirect',
],
...
}
ログインしてないとログインページとコールバックページ以外はログインページへリダイレクトしてます。
やや特殊な書き方になっちゃった理由については以下参照ください(window.location.hrefとか...)
ログイン済みかどうかはVuexの値でチェックしてるのでセキュリティ的に微妙ですがこのサイト自体にIP制御やらBasic認証やらかけるのでいいでしょう・・・。(もちろんAPIは通信できません)
ログアウト
# components/AppToolbar.vue
...
<script>
export default {
...
methods: {
async handleLogout() {
await $axios.$delete('auth/sign_out')
this.$cookies.removeAll()
this.$store.commit('user', {})
this.$store.commit('auth', null)
this.$router.push('/login')
},
...
},
...
}
</script>
APIにトークン消すリクエスト出してクッキーとVuexを初期化してるだけです。
ログアウトの共通処理作ったりログアウト用mutations追加とかするとよりよさそうです。
Rails側の改修
前回の記事で作りましたが、ちょっとだけ改修する部分がありました。
change_headers_on_each_requestについて
これ、当初の予定ではtrueにしてトークンを毎回変えてく予定でしたがなくしました。(falseへ)
trueでもログインしてAPI飛ばして認証できてるところまで確認できたのですが、ログアウトするとエラー出たり複数端末でログインすると片方切れたりと・・・、改善の調査に凄い時間かかりそうだったので無しにしました。
いい方法あれば教えてください🙏
返すユーザー情報のフィルタ
# app/models/user.rb
# frozen_string_literal: true
class User < ActiveRecord::Base
extend Devise::Models
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable,
:omniauthable
include DeviseTokenAuth::Concerns::User
def token_validation_response
as_json(only: [:id, :uid, :allow_password_change, :name, :nickname, :image])
end
end
token_validation_responseをオーバーライドして、持ってくるデータをフィルターしました。
is_adminとかトークンとか持ってきちゃってたのが嫌だったので。
これにて完了です。
ログインの様子
次はこの管理テンプレートでいらないもの消してってモンスターのCRUDテーブル実装してこうかなと思います。それでは👋😉
この記事が気に入ったらサポートをしてみませんか?