今回達成すること
今回は、ログイン前後のリダイレクト処理を行います。
また、ログイン前のユーザーがアクセスしようとしたURLを記憶し、ログイン直後に記憶ルートに遷移する実装も行います。
リダイレクト処理の設計
- ログイン前のユーザーの場合、
- ログイン後ページにアクセスしようとすると、ログインページにリダイレクトします。
- その時のルートは、Vuexに保存します。
- ログイン後のユーザーの場合、
- ログインページ、会員登録ページにアクセスすると、ホームにリダイレクトします。
ログイン直後の実装
ログインが成功すれば、
- ユーザーがアクセスしようとした記憶ルートにリダイレクトします。
- 上記ルートがなければトップページにリダイレクトします。
これらは、Nuxt.jsのミドルウェアを使って実装を行います。
(コラム)middlewareとは
middleware(ミドルウェア)
には、ページをレンダリングする前に実行する関数を指定します。
ライフライクルは、nuxtServerInit
の後、asyncData
の前に実行されます。
ファイルは「middleware」ディレクトリ内で管理します。
middleware/authenticated.js(参考コード)
export default function ({ store, redirect }) {
if (!store.state.authenticated) {
return redirect('/login')
}
}
middleware
の指定方法は3つあります。
1. プロジェクト全体で指定する
全てのページ遷移で呼び出す場合は、
登録方法は、router.middleware
プロパティにファイル名を文字列で指定します。
nuxt.config.js
export default {
router: {
middleware: 'authenticated'
// 複数ファイル指定の場合は配列で
// middleware: ['authenticated', 'stats']
}
}
2. 特定のページで指定する
ページ単位で指定する場合は、Vueファイルのmiddleware
プロパティにファイル名を指定します。
レイアウト単位でも指定可能です。
pages/index.vue
<script>
export default {
middleware: 'authenticated'
}
</script>
3 .特定のページで関数を指定する
小さい処理や、使い回さない処理であれば直接関数を書くことができます。
pages/index.vue
<script>
export default {
middleware({ store, redirect }) {
if (!store.state.authenticated) {
return redirect('/login')
}
}
}
</script>
呼び出し順序
優先度は、
nuxt.config.js のミドルウェア- レイアウトファイルのミドルウェア
- ページファイルのミドルウェア
となります。
参考: ミドルウェア - Nuxt.js
(コラム終わり)
ログイン前のユーザーのリダイレクト処理
それでは、ログイン前のユーザーをリダイレクトする処理を実装します。
「middleware」ディレクトリ内に
root $ touch front/middleware/authenticator.js
レイアウトにミドルウェアを追加する
ログイン後のページに使用するレイアウトファイル
default.vue とloggedIn.vue に
middleware
を追加します。
front/layouts/loggedIn.vue
...
<script>
export default {
// 追加
middleware: 'authenticator'
}
</script>
front/layouts/default.vue
...
<script>
export default {
// 追加
middleware: 'authenticator'
}
</script>
トップページは、レイアウト決定前にログイン判定を行いたいので、middleware
を追加します。
front/pages/index.vue
...
<script>
import homeImg from '~/assets/images/loggedIn/home.png'
export default {
// middleware 追加
middleware: 'authenticator',
layout ({ $auth }) {
return $auth.loggedIn ? 'loggedIn' : 'welcome'
},
...
}
</script>
トップページの処理
トップページは、ログイン前後の双方で使用するため、個別に処理が必要です。
トップページかつ、Vuexのユーザーが存在しない場合、何もしない処理を追加します。
この処理を追加しないと、ウェルカムページにアクセスできなくなります。
front/middleware/authenticator.js
export default async ({ $auth, store, route, redirect }) => {
// トップページかつユーザーが存在しない場合、何もしない(layouts/welcome.vue表示のため)
if (route.name === 'index' && !$auth.isUserPresent()) {
return false
}
}
有効期限が切れている時の処理
アクセス時に有効期限が切れている場合
-
Vuexのユーザーが存在する時は
- ログアウト処理をし、Vuexのユーザーを
null
にします。
- ログアウト処理をし、Vuexのユーザーを
-
Vuexのユーザーが存在しない時は
- アクセスしようとしたルートを記憶します。
-
共通処理として
- トースターの出力と、
- ログインページへリダイレクトします。
これらの処理を
なお、トースターは先の記事で作成します。
front/middleware/authenticator.js
export default async ({ $auth, store, route, redirect }) => {
// トップページかつユーザーが存在しない場合、何もしない(layouts/welcome.vue表示のため)
if (route.name === 'index' && !$auth.isUserPresent()) {
return false
}
// 以下、追加
// トップページでユーザーが存在する場合はここを通過する
if (!$auth.isAuthenticated()) {
// 有効期限外の時
let msg = 'ログインが必要です'
if ($auth.isUserPresent()) {
// ログイン中のユーザー
msg = 'もう一度ログインしてください'
await $auth.logout()
} else {
// ログイン前ユーザー
store.dispatch('getRememberRoute', route)
}
// TODO トースター出力
console.log(msg)
return redirect('/login')
}
}
認証エラーを追加する
実装上ありえないですが、有効期限内にもかかわらずユーザーが存在しない場合の処理も行います。
これは、有効期限が偽造されたケースが考えられるので、認証エラーを吐くようにします。
front/middleware/authenticator.js
export default async ({ $auth, store, route, redirect }) => {
...
if (!$auth.isAuthenticated()) {
...
return redirect('/login')
// 追加
} else if (!$auth.isUserPresent()) {
// 有効期限内でユーザーが存在しない場合
return $auth.unauthError()
}
}
unauthError()
を
front/plugins/auth.js
...
class Authentication {
constructor (ctx) {
this.store = ctx.store
this.$axios = ctx.$axios
// 追加
this.error = ctx.error
}
...
// ログアウト業務
logout () {
...
}
// 追加
// 認証エラー処理
unauthError () {
this.removeStorage()
throw this.error({ statusCode: 401, message: 'Unauthorized' })
}
}
// error 追加
export default ({ store, $axios, error }, inject) => {
inject('auth', new Authentication({ store, $axios, error }))
}
throw(スロー)
... 処理をストップする。エラー出力以後の処理は実行されません。
ログイン前のルートを記憶する
- アクション
- ミューテーション
- ステート
をそれぞれ追加しましょう。
front/store/index.js
export const state = () => ({
...
projects: [
...
],
// 追加
rememberRoute: {
name: 'index',
params: {}
}
})
export const getters = {}
export const mutations = {
...
setCurrentUser (state, payload) {
...
},
// 追加
setRememberRoute (state, payload) {
state.rememberRoute = payload
}
}
export const actions = {
...
// 現在のユーザーを設定する
getCurrentUser ({ commit }, user) {
...
},
// 追加
// ログイン前にアクセスしたルートを記憶する
getRememberRoute ({ commit }, route) {
route = route || { name: 'index', params: {} }
commit('setRememberRoute', { name: route.name, params: route.params })
}
}
ログインページにリダイレクト処理を追加する
リダイレクト先は、上記で追加した記憶ルートにリダイレクトします。
front/pages/login.vue
...
<script>
export default {
...
methods: {
...
// ログイン成功
authSuccessful (response) {
this.$auth.login(response)
// 追加
this.$router.push(this.$store.state.rememberRoute)
},
...
}
}
</script>
リダイレクトエラーを回避する
今現在
- http://localhost:8080/account と
- http://localhost:8080/project に
アクセスすると、404エラーが出るよう設定されています。
ログイン後のリダイレクト直後に、いきなり404エラーが出るとユーザーにストレスがかかります。
そこで、トップページへリダイレクトするよう変更を行います。
front/pages/account.vue
...
<script>
export default {
layout: 'loggedIn',
// 追加
middleware ({ route, redirect }) {
if (route.name === 'account') { return redirect('/') }
}
// 削除
// validate ({ route }) {
// return route.name !== 'account'
// }
}
</script>
front/pages/project.vue
...
<script>
export default {
// route, redirect追加
async middleware ({ params, store, route, redirect }) {
// 追加
if (route.name === 'project') { return redirect('/') }
return await store.dispatch('getCurrentProject', params)
},
...
}
</script>
認証前のリダイレクト処理を確認しよう
リダイレクト処理の確認事項を整理します。
ログイン前のユーザー
- コンソールに「ログインが必要です」が出力されているか
- 記憶ルートにリダイレクトされているか
- 記憶ルートが
/account
、/settings
の場合、ホームにリダイレクトされているか
コンソールに「ログインが必要です」が出力されているか
ログイン前の状態で、http://localhost:8080/project/2/dashboard にアクセスしてみましょう。
このルートには、レイアウトファイル
ログインページにリダイレクトされ、コンソールも期待通りです。
記憶ルートにリダイレクトされているか
これでルートが記憶されたか確認します。
そのままログインしてみましょう。
記憶ルートにリダイレクトされました。
記憶ルートがaccount、projectの場合、ホームにリダイレクトされているか
それでは、
- http://localhost:8080/account
- http://localhost:8080/project
それぞれにアクセスし、ホームにリダイレクトされていることも確認しておきましょう。
ログイン後のユーザー
ログイン後のユーザーの確認事項は
-
コンソールに「もう一度ログインしてください」が出力されているか
-
ログアウトが成功しているか
この2点です。
ログアウトとは、Cookie、ストレージ、ユーザーが削除されている状態です。
コンソールに「もう一度ログインしてください」が出力されているか
ログイン状態で、ローカルストレージの有効期限を削除します。
そのまま、リロードせずにどこかにページ遷移してください。
期待通りに動いていますね。
ログアウトが成功しているか
- ローカルストレージの有効期限
- Cookieのトークン
が削除されていることを確認してください。
有効期限内でユーザーが存在しない場合
特殊ですが、有効期限内でユーザーが存在しない場合もテストしておきましょう。
null
にするテストコードを追加します。
front/middleware/authenticator.js
export default async ({ $auth, store, route, redirect }) => {
...
// 追加
store.dispatch('getCurrentUser', null)
// トップページでユーザーが存在する場合はここを通過する
if (!$auth.isAuthenticated()) {
...
} ...
}
もう一度ログインすると、認証エラーが発生しました。
認証エラーが出た瞬間はCookieのトークンは残っていますが、先で実装するエラーページでCookieを削除します。
確認が済んだらテストコードは削除しましょう。
front/middleware/authenticator.js
export default async ({ $auth, store, route, redirect }) => {
...
// store.dispatch('getCurrentUser', null) 削除
...
}
ログイン前のユーザーリダイレクト処理は以上です。
ログイン後のユーザーをリダイレクトする
ログイン後のユーザーは、
- ログインページ
- 会員登録ページ
にアクセスすると、トップページにリダイレクトする処理を追加します。
「middleware」ディレクトリ内に
root $ touch front/middleware/loggedInIsRedirects.js
ルーター名がsignup
とlogin
の時にリダイレクト処理を行います。
front/middleware/loggedInIsRedirects.js
export default ({ $auth, route, redirect }) => {
// ログイン後ユーザーのリダイレクト処理
const loggedInUserNotAccess = ['signup', 'login']
if ($auth.loggedIn && loggedInUserNotAccess.includes(route.name)) {
return redirect('/')
}
}
レイアウトファイルに登録する
会員登録ページとログインページのレイアウトファイルは、
middleware
プロパティを追加しましょう。
front/layouts/beforeLogin.vue
<script>
export default {
// 追加
middleware: 'loggedInIsRedirects'
}
</script>
ログイン後のリダイレクト処理を確認しよう
ログイン状態で、
- http://localhost:8080/login
- http://localhost:8080/signup
にアクセスしてください。
トップページにリダイレクトされたら成功です。
コミットします
今回の実装は以上です。
コミットしておきましょう。
root $ cd front
front $ git add -A && git commit -m "add_redirect_middleware" && cd ..
root $
まとめ
今回はログイン前後のリダイレクト処理を行いました。
これで、ログイン前のユーザーとログイン後のユーザーのアクセス制限が実装できました。
ここまでの実装で、
- 無効な期限はページ遷移毎に削除されます。
- 有効な期限を偽造しても、Vuexにユーザーが存在しない限りログイン後ページは表示できません。
- Cookieにアクセストークンが存在しない場合、リロード毎に有効期限は
nuxtClientInit.js で削除されます。 - Vuexのユーザーを偽造しても、プロジェクト一覧を取得する際にトークンを持っていないので、Rails側でエラーを吐きます。
よし、だいぶセキュリティが固まってきましたね。
予告
ログイン失敗時のトースターを作成します。
あの「ユーザーが見つかりません」みたいなメッセージですね。
今回作成した
お楽しみに。