本文書ではNuxt.jsのモジュールnuxtjs/authを使い、Tokenを利用したJWT(JSON WEB TOKEN)のAuth(認証)の設定を行っています。Nuxt.jsでJWTの認証を行うためにはバックエンドが必要になります。バックエンドにはNodeのExpress.jsを使って構築します。本文書を読むことでNuxt.js環境下でTokenを利用したユーザ認証の設定手順の流れを理解することができます。

Tokenを使った認証の設定が行えることを本書の目的としているため入力フォームのデザイン、バリデーションやセキュリティについての説明は行っていません。特にセキュリティについてはJWTの設定方法がわかった後にしっかりと勉強してください。

Express.jsを使ったバックエンドの設定については一緒に記述すると文書が長くなるためまたフロントエンドにNuxt.jsを利用した場合のみ利用できるものではないため別の文書として公開しています。

動作確認を行ったNuxtのバージョンは2.15.7です。バージョンアップにより公開当時とNuxt.jsの初期画面が変更になっているので文書の更新は行いましたが文書と画面内の情報に一致しない情報が含まれているかもしれません。しかし、Nuxt/authでの設定内容に変更はありません。

バックエンドサーバの設定

本文書で利用するバックエンドサーバExpress.jsの構築はは下記の文書の通り行っています。

バックエンドサーバ構築完了後に下記の処理を行うことができます。Expressサーバはポート5000で起動しておりNuxt.jsをインストールするOS上で構築しているためアクセスにはhttp://localhost:5000でアクセスすることができます。

  • http://localhost:5000/api/auth/registerにPOSTリクエスト(ユーザ名、メールアドレス、パスワード)があった場合にデータベースにユーザを登録します。
  • http://localhost:5000/api/auth/loginにPOSTリクエストでメールアドレスとパスワードが送られてきた場合認証を行い、登録したユーザと一致すればTokenを返します。
  • http://localhost:5000/api/auth/userにヘッダーに受け取ったTokenをつけてGETリクエストを送りTokenに問題がなければユーザ情報を戻します。

Nuxt.jsのインストール

npx create-nuxt-appコマンドを利用して本文書ではnuxt-secondという名前のプロジェクトを作成します。プロジェクト名には任意の名前をつけてください。


$ npx create-nuxt-app nuxt-second

nuxt.jsのインストールについては下記の文書に記述しています。インストール途中のモジュールの選択ではaxiosを選択してください。

nuxtjs/authのインストール

Nuxt.jsにはAuth用のモジュールが準備されているのでそのモジュールを利用します。npm installコマンドで@nuxtjs/authをインストールします。


 $ npm install @nuxtjs/auth
Express.jsサーバにリクエストを送る時はaxiosを利用して行います。もしNuxt.jsをインストールする際にインストールを行っていない場合はnpm install @nuxtjs/axiosでインストールしてください。
fukidashi

Nuxt.jsの設定ファイルであるnuxt.config.jsにインストールしたnuxtjs/authモジュールの情報を追加します。


** Nuxt.js modules
*/
modules: [
  '@nuxtjs/axios',  
  '@nuxtjs/auth'
],

追加した直後にnpm run dev を実行するとエラーが発生します。エラーを回避するためにはVuex用のstoreディレクトリの下にindex.jsを作成しておく必要があります。nuxtjs/authでVuexを利用しているためです。index.jsが作成済みの場合はエラーは発生しません。


FATAL  Enable vuex store by creating store/index.js.       nuxt:auth 23:39:56


   ╭─────────────────────────────────────────────────────╮
   │                                                     │
   │   ✖ Nuxt Fatal Error                                │
   │                                                     │
   │   Enable vuex store by creating `store/index.js`

nuxtjs/authの設定

エンドポイントの設定

ユーザの認証を行うためサーバ側にアクセスする必要があります。それらの設定は個別に行うのではなくnuxtconfig.jsファイルにエンドポイントとして追加します。nuxt.js/authのドキュメントに記述されている内容を最初はコピー&ペーストして使います。


auth: {
  strategies: {
    local: {
      endpoints: {
        login: { url: '/api/auth/login', method: 'post', propertyName: 'token' },
        logout: { url: '/api/auth/logout', method: 'post' },
        user: { url: '/api/auth/user', method: 'get', propertyName: 'user' }
      },
      // tokenRequired: true,
      // tokenType: 'bearer'
    }
  }
}
ドキュメント通りに設定していますが、後ほど値を変更します。
fukidashi

バックエンドサーバにはaxiosを利用してリクエストを送信します。またバックエンドサーバは構築済みなのでaxiosのbaseURLにサーバのURLを設定しておきます。


axios: {
  baseURL: 'http://localhost:5000/',
},

ログイン処理の設定

通常ではユーザの登録が必要ですが、Expressサーバの構築時にユーザ作成が完了しているのでそのユーザを利用してログイン処理の動作確認を行います。

ログインフォームの作成と動作確認

ログインフォームを記述したlogin.vueファイルをpagesフォルダの中に作成します。メールアドレスとパスワードを入力できるinput要素を2つ追加します。


<template>
<div>
  <div>
  <h1>ログインユーザ</h1>
  <form @submit.prevent="loginUser">
    <div class="form-group">
      <label for="email">Email:</label>
      <input v-model="user.email">
    </div>
    <div class="form-group">
      <label for="password">Password:</label>
      <input type="password" v-model="user.password">
    </div>
    <button type="submit">ログイン</button>
  </form>
  </div>
</div>
</template>

<script>
  export default {
    data(){
      return {
        user:{
          email:'',
          password:''
        }
      }
    },
    methods:{
      loginUser(){
        this.$auth.loginWith('local',{
          data:this.user
        })
      },
    }
  }
</script>

submitボタンにはクリックイベントを設定し、ログイン情報入力後にクリックするとloginUserメソッドが実行されます。通常はsubmitを実行するとページのリロードが発生しますがページのリロードを発生させないようにpreventを設定しています。loginUserメソッドの中にログインに関する処理を記述していきます。

nuxtjs/authを利用しているので、this.$auth.loginWithを追加して引数に送信するuser情報を設定するだけで設定は完了です。loginWithの引数のlocalはnuxt.config.jsのendpointsで追加したstrategiesのlocalに対応します。これらの情報が送信される先はnuxtcofig.jsで設定したloginのurlになります。


methods:{
  loginUser(){
    this.$auth.loginWith('local',{
      data:this.user
    })
  },
}

ここまでの設定でExpress.jsサーバ側にログインフォームから送信するユーザ情報の入ったPOSTリクエストが送信されるのか確認してみましょう。

Express.jsのindex.jsファイルの/api/auth/loginに下記を追加してPOSTリクエストで送られてくる内容を表示します。


app.post('/api/auth/login/',(req,res) => {
  return console.log(req.body)
  const sql = 'select * from users where email = ?'
  const params = [req.body.email]

app.post('/api/auth/login', async (req, res) => {
  try {
    return console.log(req.body)
    const user = await User.findOne({ email: req.body.email });
    if (!user) res.json({ message: 'user not found' });
ログインを行う
ログインを行う

EmailとPasswordを入力してログインボタンを押してもブラウザ上には何も変化はありません。その場合はデベロッパーツールのコンソールを確認し、エラーが発生していないか確認します。今回は、corsのエラーが発生していることが確認できます。


Access to XMLHttpRequest at 'http://localhost:5000/api/auth/login' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
corsはCross-Origin Resource Sharing(クロスドメインリソース共有)の略です。今回はbackend, frontendともに同じドメインのlocalhostですが、ポートと番号が異なるためアクセスの許可が与えられずエラーが発生しています。
fukidashi

Express.js側でのcorsのインストールを行う必要があります。


 $ npm install cors

Express.jsファイルのindex.jsファイル内でcorsをrequireを使って読み込み設定を行います。下記の設定ではすべてのAPIからのcorsの許可を行なっています。


const cors = require('cors')
app.use(cors())

corsの設定が完了したので、Nuxt.jsからExpress.jsへのアクセスが可能となります。再度ログインフォームにメールアドレスとパスワードを入れてログインボタンを押すとExpress.jsを起動しているコンソールに送信した内容が表示されます。nuxt.config.jsの設定通りにExpress.jsサーバ側の/api/auth/loginにPOSTリクエストが送信されていることが確認できました。


[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Example app listening on port 5000!
Connected to the SQlite database.
{ email: 'kevin@test.co', password: 'password' }

Express.js側の設定が完了しているのでEmailとパスワードに間違いがなければユーザ認証は問題なく行われ、サーバ側からTokenが戻されます。

Tokenを確認

ユーザの認証が完了しTokenを受け取るとそのままnuxt.config.jsのendpointで設定したuserのurlである/api/auth/userにアクセスしてユーザ情報を取得します。正常に動作するか確認するためにuser情報を受け取る前に/api/auth/userの処理でヘッダーを確認しToken情報が含まれているのか確認しておきます。

/api/auth/userにアクセスする時はログイン処理で受け取ったTokenをサーバ側に送信するので送られてくるGETリクエストのヘッダーを確認してみましょう。


app.get('/api/auth/user/',(req,res) => {

  const headers = req.headers
  return console.log(headers)

  const bearToken = req.headers['authorization']

app.get('/api/auth/user/', async (req, res) => {
  try {
    const headers = req.headers
    return console.log(headers)
    const bearToken = await req.headers['authorization'];

ログインフォームにEmailとパスワードを入れてログインボタンを押すとExpress.jsサーバを起動(npx nodemon index.js)しているコンソールにヘッダー情報が表示されます。authorizationに Bearer + トークンが設定されていることが確認できます。


Example app listening on port 5000!
Connected to the SQlite database.
{
  host: 'localhost:5000',
  connection: 'keep-alive',
  accept: 'application/json, text/plain, */*',
  origin: 'http://localhost:3000',
  authorization: 'Bearer ' +
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibmFtZSI6ImtldmluIiwiZW1haWwiOiJrZXZpbkB0ZXN0LmNvbSIsImlhdCI6MTU3NzE2NTYyN30.w0KtYx_PVPNtKfnQK_4qPzFgOz3wj4YM5eXmibOKh8c',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) ' +
    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 ' +
    'Safari/537.36',
  'sec-fetch-site': 'same-site',
  'sec-fetch-mode': 'cors',
  referer: 'http://localhost:3000/login',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ja,en-US;q=0.9,en;q=0.8',
  'if-none-match': 'W/"4a-QvaCKH3FNt9xY+2bZiBbcCeAdC0"'
}

もし認証できない誤ったメールアドレスとパスワードでアクセスした場合は、Bearerはundefinedと表示されます。


{
  host: 'localhost:5000',
  connection: 'keep-alive',
  accept: 'application/json, text/plain, */*',
  origin: 'http://localhost:3000',
  authorization: 'Bearer undefined',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) ' +
    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 ' +
    'Safari/537.36',
  'sec-fetch-site': 'same-site',
  'sec-fetch-mode': 'cors',
  referer: 'http://localhost:3000/login',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ja,en-US;q=0.9,en;q=0.8',
  'if-none-match': 'W/"4a-QvaCKH3FNt9xY+2bZiBbcCeAdC0"'
}

Tokenを使ってuser情報を取得した後

ユーザのログイン認証が完了してTokenを取得し、Tokenを使ってユーザ情報を取得した後Nuxt.js上でどのようにユーザ情報にアクセスできるのかを確認していきます。

$authを使ってログイン状態やユーザ情報にアクセスすることができます。

ログイン状態の確認

ログイン状態の確認は、$auth.loggedInで確認できます。componentsフォルダにあるTutorial.vueファイルを開いて{{ $auth.loginIn }}を追加します。ログインページへのリンクも設定しておきます。


<!-- Please remove this file from your project -->
<template>
  <div
    class="relative flex items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0"
  >
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
      <h2>ログイン状態:{{ $auth.loggedIn }}</h2>
      <div>
        <nav>
          <nuxt-link to="/login">Login</nuxt-link>
        </nav>
      </div>

ブラウザで確認するとログインが行われていない状態では$auth.loggedInはfalseになっていることが確認できます。

ログイン情報を表示
ログイン情報を表示

Loginリンクからログイン画面に移動してログインを行います。ログインが完了すると自動で”/”にリダイレクトされます。

ログインを行う
ログインを行う

ログインが完了すると自動で/(ルート)にリダイレクトされますがもし別の場所にリダイレクトさせたい場合はnuxt.config.jsファイルで行うことができます。redirectのhomeでリダイレクトさせたい場所に変更してください。下記では/dashboardのリダイレクトされます。


auth: {
  redirect: {
    home: "/dashboard"
  },

リダイレクトされたトップのページではログインが完了しているので$auth.loggedInがtrueに変わります。

ログインが行われた場合
ログインが行われた場合

ユーザ情報へのアクセス

ログインしているユーザへのアクセスは$auth.userで行うことができます。ログインした状態であれば{{ $auth.user }}でユーザ情報に入っている情報を確認することができます。


<h2>ログイン状態:{{ $auth.loggedIn }}</h2>
<p>{{ $auth.user }}</p>
ユーザ情報を表示
ユーザ情報を表示

ユーザ情報について$store.state.auth.userからもアクセスすることができます。保持している情報は$auth.userと同じです。

ローカルストレージとCookieを確認

Token情報がどこに保存されているのか確認してみましょう。確認するためにはChromeのデベロッパーツールを利用します。ツールのApplicationタグを選択してLocal Storageを確認してください。auth_token.localにTokenが設定されていることが確認できます。

ローカルストレージ
ローカルストレージ

また、Cookiesの中身も確認してみましょう。Local Storageと同様にCookiesの中にもTokenの情報が保存されていることが確認できます。

Cookieの中身を確認
Cookieの中身を確認

Local Storage, Cookieの設定はnuxt.config.jsファイルで無効にすることも可能です。


auth: {
  cookie: false,
  // localStorage: false,
}
cookie, localStorageどちらもfalseにした場合はVuex以外に情報を保存する場所がないのでページのリロードを行うとログイン情報がないためログアウト状態になります。Vuexはメモリ上に存在するためリロードすると値を保持することはできません。
fukidashi

Chromeブラウザでdevtoolのプラグインを入れている場合はVuexを利用しているのでstateの中のauth.userにユーザ情報が保存されていることが確認できます。

Vuexでユーザ情報の確認
Vuexでユーザ情報の確認

ログアウト処理

ログアウト処理は$auth.logout()を実行することでLocal StorageとCookieに保存されているTokenを削除することで完了します。ログインしている場合はログアウトボタンを表示させています。


<h2>ログイン状態:{{ $auth.loggedIn }}</h2>
<p>{{ $store.state.auth.user }}</p>
<div>
  <nav>
  <nuxt-link to="/login">Login</nuxt-link>
  </nav>
</div>
<div v-if="$auth.loggedIn">
  <button @click="$auth.logout()">Logout</button>
</div>

$auth.logout()を実行した時にローカルでTokenを削除するだけではなくバックエンドサーバへアクセスすることも可能です。ユーザがログアウトした時にサーバ側でも何か実行したいという時に利用することができます。そのためnuxt.config.jsのendpointsのlogoutにサーバ側にアクセスするURLを設定することができます。


logout: { url: '/api/auth/logout', method: 'post' },

しかし、今回はlogout後にサーバ側での処理は必要ないのでfalseを設定します。falseを設定すると$auth.logout()を実行した時にバックエンドのサーバへのアクセスはなくCookieとLocal StorageからTokenの削除のみ行われます。


logout: false,
ログイン中なのでログインボタン表示
ログイン中なのでログインボタン表示

logoutボタンをクリックするとログイン状態はfalseに変更。

ログアウト状態
ログアウト状態

CookiesとLocal Storageを確認すると値が消えていることが確認できます。

Local StorageのTokenの値はなし
Local StorageのTokenの値はなし
Cookieの値もなし
Cookieの値もなし

もしnuxt.config.jsでlogoutを設定したままの場合はコンソールにPOSTリクエストのエラーが表示されます。


POST http://localhost:5000/api/auth/logout 404 (Not Found)

ここまでの動作確認を通してバックエンドサーバから取得したTokenはlogoutを行うまでブラウザのLocal StorageとCookiesに保管されていることがわかりました。ブラウザを一度閉じて再度ブラウザを起動後にアクセスしてください。ログイン状態が保存されていることが確認できます。

Tokenの有効期限はデフォルトでは1800s(30分)に設定されています。変更はnuxt.config.jsファイルのmaxAgeで行うことができます。この値はサーバ側でTokenに設定された有効期限がうまくデコードできない場合に利用されるとドキュメンとに記述されれいます。
fukidashi

ユーザ登録

これまでは登録済みのユーザを利用して認証の動作確認を行ってきましたがここではNuxt.jsからのユーザ登録方法について確認を行っていきます。

ユーザ登録フォームの作成

ログインフォームを元にユーザ登録フォームを作成していきます。pagesフォルダの中にregister.vueファイルを作成します。

ユーザ登録フォームにはユーザ名とメールアドレスとパスワードの3つの項目を用意します。入力フォームにユーザを登録後、登録ボタンを押すとregisterUserメソッドが実行されます。


<template>
<div>
  <div>
  <h1>ユーザ登録</h1>
  <form @submit.prevent="registerUser">
    <div class="form-group">
      <label for="name">Name:</label>
      <input v-model="user.name">
    </div>
    <div class="form-group">
      <label for="email">Email:</label>
      <input v-model="user.email">
    </div>
    <div class="form-group">
      <label for="password">Password:</label>
      <input type="password" v-model="user.password">
    </div>
    <button type="submit">登録</button>
  </form>
  </div>
</div>
</template>

<script>
  export default {
    data(){
      return {
        user:{
          name:'Steve Harry',
          email:'steve@test.com',
          password:'password'
        }
      }
    },
    methods:{
      registerUser(){
        this.$axios.post('/api/auth/register',this.user)
                    .then((response) => {
                        this.$auth.loginWith('local',{
                            data: this.user
                    })
        })
      },
    }
  }
</script>

registerUserメソッド内ではExpress.js側にある/api/auth/registerに対してaxiosを利用してPOSTリクエストを送信します。POSTリクエストでユーザ登録が完了したら、そのままログイン処理を行うだけです。ログイン処理がわかっていれば難しい箇所はありません。

新しいユーザ情報を入力して登録ボタンを押すとそのままログインまで完了し”/”にリダイレクトされ登録したユーザ情報が画面に表示されます。

ユーザ登録画面
ユーザ登録画面
ユーザ登録後の画面
ユーザ登録後の画面

middlewareでアクセス制限

ログインしていないユーザに表示させたくないページがある場合にmiddlewareを利用するとアクセスの制限を行うことができます。

新たに新規にpagesディレクトリの下にAbout.vueファイルを作成します。Nuxt.jsなのでpagesディレクトリの下にファイルを作成すると自動でルーティングを設定してくれます。


<template>
    <h1>これはAboutページです。</h1>
</template>
<script>
export default {
 
}
</script>

ログインしているしていないに関わらず/aboutにアクセスすると以下のページが表示されます。

Aboutページを表示
Aboutページを表示

このページをログインしているユーザのみしかアクセスできないようにmiddlewareを使って制限します。設定はシンプルでAbout.vueファイルのscriptタグの中に以下を追加するだけです。nuxtjs/authの機能です。


export default {
    middleware: 'auth'
}

設定を行った後にログインしていないユーザでアクセスすると/loginにリダイレクトされます。リダイレクト先の/loginはデフォルトの設定値です。

ログイン画面が表示されるので、ログインを行うと/aboutページにアクセスすることが可能になります。

すべてのページに一括で制限を行う

先程の場合は/aboutのみ個別でアクセスの制限を行いました。一括で設定を行いたい場合は、nuxt.config.jsで下記の設定を行う必要があります。


  router: {
    middleware: ['auth']
  },

設定を行うとログインしていない場合は/aboutにアクセスすると/loginにリダイレクトされます。

nuxt.config.jsで一括に制限を行った時、ある特定のページのみログインしていてもしてなくてもアクセスを許可するためには下記の設定をページに設定します。


export default {
    auth: false
}

Nuxt/authを利用することでバックエンドサーバでTokenを作成する仕組みが整っていればユーザのアクセス制限を簡単に行うことがわかりました。バックエンドサーバ上のルーティングに変更があったとしてもnuxt.config.jsを利用することで柔軟に設定変更することができるのでどのような環境でも設定は行えるはずです。