LoginSignup
36
40

More than 5 years have passed since last update.

Nuxt.jsとPassport.jsでマイクロサービス用の認証機能をもったフロントエンド + BFFを構築する

Last updated at Posted at 2019-02-16

2019/02/27 追記

SSR(サーバーサイドレンダリング)を利用した方が実装しやすそうなので、SSRを利用する形の記事も書く予定です。
そんなに差分なかったので、ブランチとして実装しておきました。
SSR版はこちら

変更点

  • mode: spaを削除
  • storeにnuxtServerInitを追加
  • 未認証時のリダイレクトロジックを修正

tl;dr

  • マイクロサービスのフロントエンドとBFF(Backends for Frontends)をNuxt.js + Express.jsで実装したかった
  • Express.js側で認証するのが都合が良かったのでPassport.jsを採用した
  • リロード時にsessionからログイン情報を再取得するように実装してみた

構成

構成は下記のようになります。

  • Nuxt.jsのserverMiddlewareとしてExpress.jsを組み込む
  • /api以下のパスはExpress.jsへルーティングし、他はNuxt.jsのSPAで画面を表示する
  • 認証のパスは/api/auth以下に実装し、Express.js上のPassport.jsで行う
  • Express.jsをBFFとし、他のマイクロサービスとの通信を行う想定とする node_bff.png

方針

  • フロントエンドにはNuxt.jsを利用
  • なるべくNuxt.jsのフレームワーク、モジュールを利用する
  • BFFとして利用するために、サーバーサイドで認証

環境

  • node 11.9.0
  • yarn 1.13.0
  • nuxt 2.4.0
  • express 4.16.4
  • passport 0.4.0

サンプルコード

プロジェクトの作成

まずは、Create Nuxt Appを利用してプロジェクトを作成します。

npxでもyarnでも実行可能ですが、この記事ではyarnを利用します。
プロジェクト名はnuxt-bff-sampleとしています。

 $ yarn create nuxt-app nuxt-bff-sample

対話形式で行うプロジェクトの設定は下記のように行います。

? Project name nuxt-bff-sample
? Project description microservice front + BFF sample app.
? Use a custom server framework none
? Choose features to install Linter / Formatter, Axios
? Use a custom UI framework none
? Use a custom test framework none
? Choose rendering mode Single Page App
? Author name reireias
? Choose a package manager yarn

作成したプロジェクトのディレクトリへ移動し、アプリをサーバーをdevelopment環境として起動してみます。
(Nuxt.jsはdevelopment環境ではホットリロードが有効になっており、ソースファイルが更新されると自動でブラウザ側に反映されるようになっています)

$ cd nuxt-bff-sample
$ yarn dev

起動したら http://localhost:3000 へアクセスしてみましょう。
nuxt-01.png

GitHub認証の準備

GitHubを利用したOAuth認証を行うためにGitHub側で設定を作成します。

  1. GitHubのOAuth application作成ページを開きます。

  2. 下記画像を参考に各項目を設定します。

  3. Register applicationボタンをクリックし、アプリケーションを作成します。

  4. 作成後、Client IDClient Secretが表示されるので、控えておきましょう。

serverMiddlewareの設定

BFFとセッション管理を行うexpressサーバーを追加します。
Nuxt.jsのフレームワーク内で管理したいので、serverMiddlewareとして設定します。

まず、expressをプロジェクトへ追加します。

$ yarn add express

次に、serverディレクトリを作成し、index.jsファイルを下記のように実装します。

server/index.js
import express from 'express'

const app = express()

app.get('/hello', (req, res) => {
  res.send('world')
})

module.exports = {
  path: '/api',
  handler: app
}

nuxt.config.jsを修正してserverMiddlewareを設定します。

nuxt.config.js
...
  serverMiddleware: [
    '~/server'
  ],
...

これらの実装を追加することで、http://localhost:3000/api 以下のリクエストがserver/index.jsで処理されるようになります。

サーバーを起動し、http://localhost:3000/api/hello へアクセスしてみましょう。
worldと表示されるはずです。

Passport.jsによる認証の実装

expressサーバー上でGitHubのOAuth認証を扱うためにPassport.jsを導入します。

(サーバーサイドで簡単にユーザー情報を取得できるようにするため、Nuxt.jsのauth-moduleではなく、Passport.jsを採用しています)

まずは必要なモジュールをプロジェクトに追加します。

$ yarn add express-session passport passport-github2 body-parser

次にサーバーサイドを実装していきます。
概ねPassport.jsのドキュメントにしたがって実装しています。
GITHUB_CLIENT_IDGITHUB_CLIENT_SECRETは環境変数から受け取るように実装しています。

server/index.js
import express from 'express'
import session from 'express-session'
import passport from 'passport'
import { Strategy } from 'passport-github2'
import bodyParser from 'body-parser'

const app = express()

// requestでjsonを扱えるように設定
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

// sessionの設定
app.use(session({
  secret: process.env.SESSION_SECRET || 'secret',
  resave: true,
  saveUninitialized: true,
  cookie: {
    secure: 'auto'
  }
}))

// Passport.jsの設定
app.use(passport.initialize())
app.use(passport.session())

passport.use(new Strategy(
  {
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/callback'
  },
  (accessToken, refreshToken, profile, done) => {
    process.nextTick(() => {
      return done(null, profile)
    })
  }
))

passport.serializeUser((user, done) => {
  done(null, {
    id: user.id,
    name: user.username,
    avatarUrl: user.photos[0].value
  })
})
passport.deserializeUser((obj, done) => {
  done(null, obj)
})

app.get('/auth/login', passport.authenticate('github', { scope: ['user:email'] }))
app.get('/auth/callback',
  passport.authenticate('github'),
  (req, res) => {
    res.json({ user: req.user })
  }
)
app.get('/auth/logout', (req, res) => {
  req.logout()
  res.redirect('/')
})

module.exports = {
  path: '/api',
  handler: app
}

続いてpage/index.vue(ページ的には http://localhost:3000/ )にログインリンクを表示するように修正します。
遷移先はサーバーサイドのログインURLである/api/auth/loginを指定しています。

page/index.vue
<template>
  <section class="container">
    <div>
      <logo />
      <h1 class="title">
        nuxt-bff-sample
      </h1>
      <h2 class="subtitle">
        microservice front + BFF sample app.
      </h2>
      <div class="links">
        <a
          href="/api/auth/login"
          class="button--green"
        >Login with GitHub</a>
      </div>
    </div>
  </section>
</template>

...

次はログイン後のユーザー情報を格納するvuexのstoreを実装します。
storeに格納することで、SPAの任意のページからこのuserオブジェクトへのアクセスが可能となります。

store/index.js
export const state = () => ({
  user: null,
  auth: false
})

export const mutations = {
  login(state, payload) {
    state.auth = true
    state.user = payload
  },
  logout(state) {
    state.auth = false
    state.user = null
  }
}

続いて、callbackページの実装です。
ここはGitHubにCallback URLとして設定したパスに相当します。
mountedでページが読み込まれたら、express側の/api/auth/callbackを呼び出し、ユーザーデータをPassportから取得し、上で用意したvuexのstoreに格納します。

page/callback.vue
<template>
  <p>callback</p>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  async mounted() {
    const res = await this.$axios.get('/api/auth/callback', {
      params: this.$route.query
    })
    const user = {
      id: res.data.user.id,
      name: res.data.user.username,
      avatarUrl: res.data.user.photos[0].value
    }
    this.$store.commit('login', user)
    this.$router.push('/home')
  }
}
</script>

最後にログイン時しか表示できないHomeページを実装します。
fetchで認証状態をチェックすることで、未認証時はルートへリダイレクトしています。
また、ログアウトボタンも実装しています。

page/home.vue
<template>
  <div>
    <h1>Home</h1>
    <p>name: {{ $store.state.user.name }}</p>
    <img :src="$store.state.user.avatarUrl" width="100px" height="100px">
    <br>
    <a
      href="/api/auth/logout"
      class="button--green"
    >Logout</a>
  </div>
</template>

<script>
export default {
  fetch({ store, redirect }) {
    // 未認証の場合、リダイレクトする
    if (!store.state.auth) {
      return redirect('/')
    }
  }
}
</script>

さて、ここまでの実装でサーバーを起動してみましょう。
GitHubのClient IDとCrient Secretを環境変数から取得すように実装したので、下記のコマンドで起動します。

$ GITHUB_CLIENT_ID=<your_client_id> GITHUB_CLIENT_SECRET=<your_client_secret> yarn dev

起動できたら、http://localhost:3000 へアクセスし確認してみましょう。
下記画像のように表示されているでしょうか?

  1. ルートページ
    login_01.png
  2. OAuth認証ページ
    login_02.png
  3. ログイン後の/homeページ
    login_04.png

ログアウトしてから、URL直指定でhttp://localhost:3000/home へアクセスすると、未認証なので/へリダイレクトされることが確認できます。

リロード時にsessionからユーザー情報をvuexのstoreに格納

さて、ログインした状態でhttp://localhost:3000/home でページをリロードすると何が起こるでしょうか?
現在の実装では、リロード後、未認証とみなされ、/へリダイレクトされてしまいます。
これはvuex(Nuxt.js)のstoreに格納してあるデータがリセットされるためです。

storeのデータをlocalStorageに保存する方法もあるのですが、セキュリティの観点からあまりよろしくない方法なので、サーバーサイドのsessionからユーザー情報を取得するように修正していきます。

まずはサーバーサイド側にsessionからuserのオブジェクトを取得して返すapiを追加します。

server/index.js
...
app.get('/session', (req, res) => {
  res.json({ user: req.user })
})
...

次に、未認証状態であった場合、上記apiを利用してサーバーサイドのsessionからuser情報を取得する機能をmiddlewareとして追加します。
Nuxt.jsのmiddlewareは(ざっくり言うと)任意の処理を各ページ内で行うように設定することが可能な機能です。

middleware/session.js
import axios from 'axios'

export default async ({ store, route, redirect }) => {
  // 認証済みの場合は何もしない
  if (store.state.auth) {
    return
  }

  if (route.path !== '/callback') {
    // サーバーのsessionからuser情報を取得する
    const res = await axios.get('/api/session')
    if (res.data.user) {
      store.commit('login', res.data.user)
      if (route.path === '/') {
        return redirect('/home')
      }
    } else if (route.path !== '/') {
      // 無限リダイレクトにならないように、パスが"/"の場合は何もしない
      return redirect('/')
    }
  }
}

最後にnuxt.config.jsにて、上記のmiddlewareを登録します。

nuxt.config.js
...
  router: {
    middleware: 'session'
  },
...

では実際に確認してみましょう。

  1. http://localhost:3000 にアクセスし、ログインする
    • /homeにリダイレクトされる
  2. ブラウザをリロードする
    • 先ほどは/へリダイレクトされてしまったが、今回の修正で/homeのままである

以下も確認できるはずです。

  • cookieを削除してリロードすると、未認証と判定され/へリダイレクトされる
  • 認証済みの状態で/へアクセスすると/homeへリダイレクトされる

BFFの実装

server/index.js以下に実装していきます。
axiosを用いた普通の実装になると思われるので、今回は割愛します。

所感

Node.jsで簡単なマイクロサービスを構築してみようと思ったら、意外とフロントエンドと認証とBFFの部分で悩んだんので記事にしてみました。
「こういったやり方もある」とか「こうした方がいい」とかあればコメントに書いていただけると参考になります。
たぶんセキュリティ的にもっと考慮すべきポイントがありそうです。

36
40
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
36
40