概要
ざっくり以下をやる
サーバーサイド
ログイン機能
ログアウト機能
セッション管理
API 処理内でのログインユーザーからのリクエストかどうかの認証
フロント
ログイン後、ストアでログインユーザー情報の保持
ストア情報をもとにフロント内でのログインユーザーかどうかの認証
自分でもよくわからなくなるので、振り返りのためにまとめるが、わかりやすくまとめる方法もよくわからないので、見返す度にちょくちょく書き直して行く。
環境
Passport
Node.js のための認証ミドルウェア。認証リクエストをおこなうための必要最低限の機能をもつように設計されているとのこと。確かに簡単で使いやすかった。
認証設定
認証用ストラテジー設定
passport-local を使用して、ユーザ名とパスワードによるローカル認証設定を行う。
const LocalStrategy = require('passport-local').Strategy;
passport.use( new LocalStrategy((username, password, done) => {
}));
return done( null, user )
return done( null, false, { message: メッセージをつけることも可能です。})
return done( error )
セッション管理設定
セッションを管理するために Passport で以下のことを行う。
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
})
})
まとめると、こんな感じ
server/passport_auth.js
import db from './models'
const LocalStrategy = require('passport-local').Strategy
module.exports = (passport) => {
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
db.users
.findOne({
where: { id: id }
})
.then(user => {
done(null, user)
})
.catch(error => {
done(error, null)
})
})
passport.use(
new LocalStrategy((username, password, done) => {
db.users
.findOne({
where: { username: username }
})
.then(user => {
if (!user) {
return done(null, false, { message: 'アカウント名が正しくありません。' })
}
if (!user.authenticate(password)) {
return done(null, false, { message: 'パスワードが正しくありません。' })
}
return done(null, user)
})
.catch(error => {
return done(error)
})
})
)
}
Express
API周り
ログイン
passport.authenticate にてユーザー認証を実行し、レスポンスを返す
passport.authenticate('local', (err, user, info) => {
if (err) {
return next(err)
}
if (!user) {
return res.status(401).send(info)
}
req.login(user, (err) => {
if (err) {
return next(err)
}
return res.status(200).json(user)
})
})(req, res, next)
ログアウト
req.logout() すればいいだけ、とてもシンプル。
req.logout()
res.status(200).send()
その他の認証
APIアクセス時に、リクエストに対してreq.isAuthenticated()で認証済みかどうか確認できる
router.get('/me', checkAuthentication, (req, res) => {
return res.status(200).json(req.user)
})
function checkAuthentication(req, res, next) {
if (req.isAuthenticated()) {
next()
} else {
res.status(204).send()
}
}
まとめると、こんな感じ
api/v1/auth.jsimport { Router } from 'express'
import passport from 'passport'
const router = Router()
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return next(err)
}
if (!user) {
return res.status(401).send(info)
}
req.login(user, (err) => {
if (err) {
return next(err)
}
return res.status(200).json(user)
})
})(req, res, next)
})
router.get('/logout', (req, res) => {
req.logout()
res.status(200).send()
})
router.get('/me', checkAuthentication, (req, res) => {
return res.status(200).json(req.user)
})
function checkAuthentication(req, res, next) {
if (req.isAuthenticated()) {
next()
} else {
res.status(204).send()
}
}
export default router
その他サーバーサイド
Passport の初期化
初期化のために以下のメソッドを実行する必要がある。
passport.initialize() :Passport の初期化
passport.session() :ログイン後のセッション管理
正しい順番でログインセッションを処理するために passport.session() は express.session() よりも後に記述する
諸々の読み込みと一緒にまとめると、こんな感じ
server/index.jsimport express from 'express'
import cookieParser from 'cookie-parser'
import bodyParser from 'body-parser'
import session from 'express-session'
import passport from 'passport'
import { Nuxt, Builder } from 'nuxt'
import api from './api/v1'
import db from './models'
const app = express()
app.use(express.static('public'))
app.use(cookieParser())
app.use(bodyParser.json())
app.use(
session({ secret: 'your secret keyword' })
)
app.use(passport.initialize())
app.use(passport.session())
db.sequelize
.authenticate()
.then(() => {
console.log('Connection has been established successfully.')
})
.catch(err => {
console.error('Unable to connect to the database:', err)
})
require('./passport_auth')(passport)
app.use('/api/v1', api)
const config = require('../nuxt.config.js')
const nuxt = new Nuxt(config)
if (nuxt.options.dev) {
new Builder(nuxt).build()
}
app.use(nuxt.render)
Nuxt.js
ストア ( Vuex )
ログインしたらログインユーザーの必要な情報をストアに保持する
ステート
const state = {
loginUser: null
}
ゲッター
const getters = {
isAuthenticated (state) {
return !!state.loginUser
},
loginUser(state) {
return state.loginUser
}
}
ミューテーション
const mutations = {
login (state, user) {
state.loginUser = user
},
logout (state) {
state.loginUser = null
}
}
アクション と nuxtServerInit
アクションはミューテーションと似ていて、使わなくてもいい場合もある。ただ、以下の点でミューテーションとは異なる。
アクションは、状態を変更するのではなく、ミューテーションをコミットするもの。
アクションは非同期処理を含むことができる。
ログイン後、必要なログインユーザーのデータを取得し、ミューテーションをコミットするようにする。
Nuxtの場合、nuxtServerInit() に任意の処理を追加できる。
nuxtServerInit() アクションは引数でコンテキストを渡して呼び出されるので、アプリケーションがロードされたときは、サーバーから取得できるデータが既にストアに入っている状態にできるようになる。
なのでreq.userから、必要なミューテーションをコミットする。
const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.user) {
return commit('login', req.user)
}
return commit('logout')
},
fetchLogin({ commit }) {
return new Promise((resolve, reject) => {
axios
.get('/me')
.then(response => {
if (response.status === 200) {
commit('login', response.data)
} else {
commit('logout')
}
resolve()
})
.catch(error => {
reject(error)
})
})
}
}
ストアの処理をまとめると、こんな感じ
store/auth.jsimport axios from '~/plugins/axios'
const state = {
loginUser: null
}
const getters = {
isAuthenticated (state) {
return !!state.loginUser
},
loginUser(state) {
return state.loginUser
}
}
const mutations = {
login (state, user) {
state.loginUser = user
},
logout (state) {
state.loginUser = null
}
}
const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.user) {
return commit('login', req.user)
}
return commit('logout')
},
fetchLogin({ commit }) {
return new Promise((resolve, reject) => {
axios
.get('/me')
.then(response => {
if (response.status === 200) {
commit('login', response.data)
} else {
commit('logout')
}
resolve()
})
.catch(error => {
reject(error)
})
})
}
}
export default {
state,
getters,
mutations,
actions
}
認証middleware
ページ遷移前に、フロント側でそのページを表示する or リダイレクトする の認証処理を行うミドルウェアを用意する。
処理の内容は簡単
middleware/authenticated.jsexport default function ({ store, route, redirect }) {
if (store.getters.isAuthenticated) {
return redirect('/')
}
}
ログイン画面
login.vue<template>
<article class="layout-login">
<section id="login">
<form @submit.prevent="authenticate">
<input id="username" name="username" placeholder="Username" type="text" autocomplete="username" v-model="auth.username">
<input id="password" name="password" placeholder="Password" type="password" autocomplete="current-password" v-model="auth.password">
<div class="error" v-if="message">{{message}}</div>
<input id="submit" name="submit" type="submit" value="Login">
</form>
</section>
</article>
</template>
<script>
import axios from '~/plugins/axios'
export default {
middleware: 'authenticated',
data () {
return {
auth: {
username: '',
password: '',
remember: true
},
message: null
}
},
methods: {
authenticate () {
axios
.post('login', this.auth)
.then(response => {
this.$store.dispatch('fetchLogin')
})
.catch(error => {
if (error.response.status === 401) {
this.message = error.response.data.message
}
console.error(error.message)
})
}
}
}
</script>
これで完成。かと思いきや。。。
上記だけではまだ不十分だった。以下の場合で不具合が起きる。
これを回避するために、リロード時のasyncData()での APIリクエストにもユーザー情報を加える。
やるのは以下の通り
nuxtServerInit({ commit }, { req }) {
if (req.user) {
axios.defaults.headers.common.cookie = req.headers.cookie
return commit('login', req.user)
}
return commit('logout')
},
余談
以上。もう少しうまくまとめられるようになりたいところです。