LoginSignup
8
9

More than 3 years have passed since last update.

Nuxt + Node(Express) + passportでsession認証機能の実装まとめ

Last updated at Posted at 2019-09-04

Nuxt+Express+passportでユーザーネームとパスワードを使った認証機能実装。
使っている関数が一体何をしているのか分からなすぎたので、実装しながらまとめました。

・記述場所はある程度分けましたが、適宜変更をお願いします。
・知識定着のアウトプット用記事なので、順を追って実装しています。
・バージョンは後述のpackage.jsonを参考にしてください。

【実装の挙動】
http://localhost:3000/signinにアクセスし、
ユーザー名:test
パスワード:123456789
を入力。
認証成功でmypage.vueに遷移
失敗でindex.vueに遷移
mypageに遷移するとcreated関数が発火し、ユーザー情報を取得し表示させる。
(nuxtではnuxtServerInitというライフサイクルがありますが、SPAでの使用はプラグインが必要なので今回は検証用でcreatedにします。)

プロジェクト作成

npx create-nuxt-app <project-name>で下記のように設定しました。
今回はexpress、axiosを使うので、あとはお好みでお願いします。

create-nuxt-app v2.10.1
✨  Generating Nuxt.js project in testApp
? Project name testApp
? Project description My smashing Nuxt.js project
? Author name UserName
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework Express
? Choose Nuxt.js modules Axios
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)

必要なモジュールのインストール

$ npm install body-parser --save
$ npm install passport --save
$ npm install passport-local --save
$ npm install express-session --save

【各々の用途】
body-parser: URLエンコードされた値をbodyに返す
passport: 認証リクエストを行う
passport-local: 認証ストラテジー作成
express-session: セッションを使用

全て上記の通りインストールしたpackage.json。

package.json
{
  "name": "testApp",
  "version": "1.0.0",
  "description": "My smashing Nuxt.js project",
  "author": "UserName",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "body-parser": "^1.19.0",
    "cross-env": "^5.2.0",
    "express": "^4.16.4",
    "express-session": "^1.16.2",
    "nuxt": "^2.0.0",
    "passport": "^0.4.0",
    "passport-local": "^1.0.0"
  },
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
}

認証機能の実装

【概要】
まず名前とパスワード認証によるルート変更の実装。

【作成するディレクトリ・ファイル】
pagesディレクトリの中に
・mypage.vue: 認証成功時の遷移先であるマイページ
・signin.vue: サインイン用のページ
(CSS当てるのがラクなので2つとも今回はpages/index.vueをコピペし少し修正しました)

・routesディレクトリ: ルーティング用のディレクトリを新しく作成。
その中に、
・mypage.js: mypageページ用のjsファイル
・signin.js: signinページ用のjsファイル

pages/mypage.vue
<template>
  <div class="container">
    <div>
      <h1 class="title">
        MyPage
      </h1>
      <h2 class="subtitle">
        {{ userName }}
      </h2>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userName: 'No Name'
    }
  },
  created() {
    this.$axios
      .$get('/mypage/user')
      .then((res) => {
        this.userName = `こんにちは、${res.name}さん`
      })
      .catch((error) => {
        console.log(error)
      })
  }
}
</script>
pages/signin.vue
<template>
  <div class="container">
    <div>
      <h1 class="title">
        SignIn
      </h1>
      <h2 class="subtitle">
        名前とパスワードを入力
      </h2>
      <form action="/signin" method="post">
        <div>
            <label>ユーザー名:</label>
            <input type="text" name="username">
        </div>
        <div>
            <label>パスワード:</label>
            <input type="password" name="password">
        </div>
        <div>
            <input type="submit" value="ログイン">
        </div>
      </form>
    </div>
  </div>
</template>
server/index.js
const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()

// 追記 ここから************************************************
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const mypageRouter = require('../routes/mypage')
const signinRouter = require('../routes/signin')
const bodyParser = require('body-parser')

//後ほどここにsession機能記述

app.use(passport.initialize())

// passportとStrategyの紐づけ
// routes/signin.jsのpassport.authenticate()によって以下の処理が走る。
passport.use('local', new LocalStrategy({
  usernameField: 'username',
  passwordField: 'password'
}, function (username, password, done) {
    // 入力された名前とパスワード照合
    if (username === "test" && password === "123456789") {
      return done(null, username)
    } else {
      console.log("error")
      return done(null, false, { message: '入力が正しくありません。' })
    }
}))

app.use(bodyParser.urlencoded({extended: false}))
app.use('/mypage', mypageRouter)
app.use('/signin', signinRouter)
// 追記 ここまで************************************************

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start () {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

routes/mypage.js
const express = require('express')
const router = express.Router()

router.get('/user', (req, res) => {
  console.log(req.user)
  if(req.user) {
    res.send(req.user)
  } else {
    console.log('セッションがありません')
  }
})

module.exports = router
routes/signin.js
const express = require('express')
const passport = require('passport')
const router = express.Router()

// ログイン処理を定義
router.post('/', passport.authenticate('local', 
{
  successRedirect: '/mypage',
  failureRedirect: "/",
  session: false
}))

module.exports = router

index.jsの解説

passport.initialize()

passportの初期化処理

passport.use()

【第一引数】
あとからストラテジーを呼び出すために使う一意な名前を指定(今回だとsignin.jsのpassport.authenticate() の引数に指定している)

【第二引数】
ストラテジーを指定(今回は一番よくつかわれると思われるLocalStrategyを指定)

LocalStrategy

インスタンス生成時に2つ引数を取る。
【第一引数】
・Field
signin.vueのフォームで指定したnameを記述(<input type="text" name="username">の「username」部分)

usernameField:ユーザー名で指定したname
passwordField:パスワードで指定したname

【第二引数】
認証処理を定義。
ユーザー名とパスワードが一致しているかどうかをチェックし、認証出来たら必要なデータだけに絞ってdone()へ引き渡す。

LocalStrategyのコールバック関数は、done()のコールバックを返すようにしていれば実装方法は任意でよく、とにかく与えられたIDとパスワードが正しいか(IDがある、パスワードが正しい、など)判断し、done()を返せばい良い。

done()
認証成功または失敗を、passport内部に対して通知するイベントを発行しています。
エラーがあったときは第1引数に値が渡ります。
第2引数はfalseを渡せば認証失敗、それ以外の値が渡れば成功となります。

また、この渡した変数がセッションに保存されるのですが、セッションからの情報書き出し取得をするserializeUser()、 deserializeUser() 関数を定義しておく必要があり、バックエンド側でreq.userにユーザー情報が格納されます。
ここはお決まりの書き方なよう。

signin.jsの解説

passport.authenticate()

localストラテジーでログイン要求を処理する

【第一引数】
passport.use()で設定したストラテジー名を指定

【第二引数】
successRedirect: 認証成功後の遷移先
failureRedirect: 認証失敗後の遷移先
session: sessionを使用するかどうか

ここまでの挙動

この実装でusernameとpasswordでユーザーを認証し、認証の可否によってルート変更が可能になります。
mypageに遷移した時にcreated関数が発火し、mypage/userにアクセスします。
ターミナルを見ると、
「console.log(req.user)」の結果はundefined
次に、「セッションがありません」
と表示されていると思います。
これは、ただルート変更が可能になっただけで入力された情報を保持していないことを意味しています。
そこでsessionを利用することになります。

session機能の追加

先ほどのindex.jsファイルで、session記述予定の場所に追記します。
必要なところだけ載せます。

server/index.js
const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const mypageRouter = require('../routes/mypage')
const signinRouter = require('../routes/signin')
const bodyParser = require('body-parser')

// 追記ここから ************************************************
const session = require('express-session')
app.use(session({
  secret: 'testing',
  resave: false,
  saveUninitialized: true
}))
app.use(passport.initialize())
app.use(passport.session())

//passportとsessionの紐づけ
passport.serializeUser(function(username, done) {
  console.log('serializeUser')
  done(null, username);
})
passport.deserializeUser(function(username, done) {
  console.log('deserializeUser')
  done(null, {name:username})
})

// 追記ここまで ************************************************

//ここにあったapp.use(passport.initialize())は上に記述した為削除

passport.use('local', new LocalStrategy({
  usernameField: 'username',
  passwordField: 'password',
}, function (username, password, done) {
    if (username === "test" && password === "123456789") {
      return done(null, username)
    } else {
      console.log("error")
      return done(null, false, { message: '入力が正しくありません。' })
    }
}))

app.use(bodyParser.urlencoded({extended: false}))
app.use('/mypage', mypageRouter)
app.use('/signin', signinRouter)

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
...

【注意点】
app.use('/mypage', mypageRouter)等のルート宣言より前に記述しないと、
Error: passport.initialize() middleware not in use
というエラーが出ます。
原因は、passportコードをルート設定の後ろに記述してしまうと、そのルートがpassportコードよりも先に走ることになる為です。
ルートを宣言する前にアプリを構成した方が良いようです。

server/signin.js
const express = require('express')
const passport = require('passport')
const router = express.Router()

// ログイン処理を定義
router.post('/', passport.authenticate('local', 
{
  successRedirect: '/',
  failureRedirect: "/signin",
  session: true //trueに変更のみ
}))

module.exports = router

session部分の解説

sessionの利用は、
session() → passport.initialize() → passport.session()
の順でコードを追加します。

session()

【基本構文】
インスタンス名.use(session({ 設定項目: '値'}))

【設定項目】
secret: 好きな文字列で大丈夫です。この文字列をキーとしてクッキーを暗号化します

resave: セッションチェックを行うたびにセッションを作成するかどうかの指定。falseにすることで、毎回セッションを作成しないようにしています。

saveUninitialized: 未初期化状態のセッションを保存するかどうかの指定。保存する場合はtrue。

passport.session()

ログイン後のセッション管理を行う記述

serializeUser(),deserializeUser()

まずこちら。
passport公式より

現在Web上にあるほとんどのアプリケーションは、都度認証を行うのではなく、認証情報をログインリクエストのときのみ送信しています。 そして、認証が成功した場合は、ブラウザ上のクッキーを利用することでセッションを確立・維持する作りになっています.。
この仕組みは、ユニークなクッキーによるセッション管理によって実現されています。 そのため、ログイン後のリクエストそれぞれに認証情報を含むことはありません。 セッションのサポートのために、パスポートは user インスタンスをシリアライズ/デシリアライズしてセッション情報として管理しています。

【シリアライズ/デシリアライズとは】
オブジェクトの状態をStreamの状態(1バイトずつ読み書きできるバイト配列状のデータ構造)に変換することをシリアライズと言うようです。逆にStreamの状態をオブジェクトに戻してやることをデシリアライズと言います。
ファイルに保存できる状態=ストリームという感じでしょうか。この状態に変換することを「シリアライズ」、それを解凍して使えるようにすることを「デシリアライズ」と呼ぶイメージでそう間違っていないかと思います。

【シリアライズの挙動】
「passport.use(new LocalStrategy({...」内の「return done(null, username)」で、usernameが「passport.serializeUser」関数のコールバック関数の第一引数に渡ります。
シリアライズ、デシリアライズ内の動作はアプリケーションによって定義されるため、アプリケーションでは認証レイヤーでの制限無しに、適切なデーターベースやオブジェクトマッパーを選択可能。

リクエストを受け取ると、IDからユーザーを特定し、req.user内に格納され、リクエストが呼ばれるたびに取得できるようになります。

実装の挙動確認

signin.vueにアクセスしユーザー名とパスワードを入力すると、mypageに遷移。
すると「No Name」だったところが、「こんにちは、testさん」に変わっていると思います。
ターミナルを確認すると

serializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
deserializeUser
{ name: 'test' }

と表示されていると思います。
serializeUserとdeserializeUserが走っていなければ失敗していますので、もう一度コードの順番など確認してみてください。
そして、console.log(req.user)の結果が{name: 'test'}になっています。
req.userの中にdone()の第二引数に設定した値が格納されている事が確認できました。

ページをリロードしても同じ処理が走り、再びユーザー情報を取得できます。
このsession機能を使い、例えばこんな感じでログインしているかが確認できるようになります。

router.get('/mypage', function(req, res) {
    if(req.user){
       // user認証済みの実装
    }else{
       // user確認できない時はホーム画面にリダイレクト
      res.redirect('/')
    }
})

また、req.logout()でログアウト処理ができます。こんな感じでしょうか。

router.get('/logout', function(req, res, next) {
    req.logout()
    res.redirect('/')
})

少し長くなりましたが以上です。
実際の実装ではデータベースに値を保存し、そのデータとサインインで入力されたデータが一致するか検証にかける流れになると思います。
こちらもインプットが進み次第まとめようと思います。

補足や訂正などありましたらご教授いただけると幸いです。
最後までご覧いただきありがとうございました。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9