NuxtJSでサーバ側はexpress、websocketを使うのにsocket.ioを使っていて、ユーザー認証をpassportで行なう場合の覚書き。
今回試すこと
axiosを使ってサーバAPIでログインし、その後socket.ioでwebsocketに接続する。やりたいことは、サーバAPIで開始したセッションをsocket.ioで引き継ぎ利用することである。
これには、socket.ioのミドルウェアを使って開始済みのセッションを読み込めば、socket.ioのリクエストでセッションを扱える。気をつける点は、socket.ioのミドルウェア関数インターフェースがexpressのそれと異なっているので、正しく情報を引き継げるようミドルウェアをつないでやることである。具体的には、socket.ioのドキュメントにもあるとおり以下のようにすればよい。
1 |
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); |
socket.ioのロード
今回は、NuxtJSのmoduleのlistenフックを使ってsocket.ioサーバをnuxtのhttpサーバ に統合する。serverMiddleware
で、/api
にexpressのappをマウントする。また、Nuxtのlistenフックを使ってsocket.ioサーバを統合するための設定をmodulesに定義する。
1 2 3 4 5 6 7 8 9 10 |
// nuxt.config.js serverMiddleware: [{ path: '/api', handler: '~/server/index.js' }], // Modules: https://go.nuxtjs.dev/config-modules modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', '~/modules/socket-io.js', ], |
1 2 3 4 5 6 7 8 |
// ~/modules/socket-io.js import { attachSocketIO } from '../server/io' export default function () { this.nuxt.hook('listen', (server, listener) => { attachSocketIO(server) }) } |
サーバ側
今回は動作検証のため、セッションはメモリストアを使っている。socket.ioとセッションストアを共有するため、共通の定義を使うようにしている。
server/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import express from 'express' import passport from 'passport' import LocalStrategy from 'passport-local' import session from './session' const app = express() // 今回はDBを使わず簡易のリストデータを使う const USERS = [ { username: 'username1', password: 'password1', }, ] // セッションのMemoryStoreはsocket.ioと共有する app.use(session) app.use(express.urlencoded({ extended: true })) app.use(express.json()) app.use(passport.initialize()) app.use(passport.session()) passport.use( new LocalStrategy(function (username, password, done) { const user = USERS.find( (data) => data.username === username && data.password === password ) if (!user) { return done(null, false) } return done(null, { username: user.username }) }) ) passport.serializeUser(function (user, done) { done(null, user) }) passport.deserializeUser(function (user, done) { done(null, user) }) app.post( '/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/', }), (req, res, next) => { res.json(req.user) } ) app.post('/logout', (req, res, next) => { req.logout() res.redirect('/') }) app.get('/session', (req, res, next) => { res.json(req.user) }) export default app |
server/session.js
1 2 3 4 |
import session from 'express-session' const ses = session({ secret: 'secret', resave: true, saveUninitialized: true }) export default ses |
server/io.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import socketIO from 'socket.io' import passport from 'passport' import session from './session' // connect io middleware to express middleware const wrap = (middleware) => (socket, next) => middleware(socket.request, {}, next) export function attachSocketIO(server) { const io = socketIO(server, { serveClient: false }) // セッションのMemoryStoreはexpressと共通にする io.use(wrap(session)) io.use(wrap(passport.initialize())) io.use(wrap(passport.session())) io.use((socket, next) => { if (socket.request.user) { next() } else { next(new Error('unauthorized')) } }) io.on('connection', (socket) => { socket.emit('echo', { message: 'hello', }) }) return io } |
クライアント側
クライアント側は、ログインしてセッション確認するだけのためのインターフェースにする。以下のような感じ。
ログインしていない状態の時。
ログインしてsocket.ioにつながった時。
Viewテンプレートはveutifyを使う。
pages/index.vue
NuxtJSは、Universalモードで動かしているのでSSRを行なう。sessionは、asyncData
フックを使ってコンポーネントのdataに統合する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
<template> <v-app> <v-main> <v-card class="ma-5 pa-5" width="400" flat> <v-text-field v-model="username" label="username" required ></v-text-field> <v-text-field v-model="password" label="password" required ></v-text-field> <v-btn :disabled="!loggedIn" @click="submit"> ログイン </v-btn> <v-alert class="my-5" :type="socket && socket.connected ? 'success' : 'error'" dense > {{ socketStatus }} </v-alert> <v-dialog v-model="dialog" width="300"> <pre class="pa-5">{{ session }}</pre> </v-dialog> <v-btn text :disabled="loggedIn" @click="getSession">session</v-btn> | <v-btn :disabled="loggedIn" text @click="logout">logout</v-btn> </v-card> </v-main> </v-app> </template> <script> import { io } from 'socket.io-client' export default { async asyncData({ app }) { const session = await app.$axios.$get('/api/session') return { session } }, data() { return { dialog: false, username: 'username1', password: 'password1', socket: null, socketStatus: 'disconnected', session: false, } }, computed: { loggedIn() { return !this.session }, }, mounted() { if (this.session) { this.connectWs() } }, methods: { async submit() { await this.$axios .$post('/api/login', { username: this.username, password: this.password, }) .then((user) => { this.session = user if (this.socket && this.socket.connected) { this.socket.close() } this.connectWs() }) }, async logout() { await this.$axios.$post('/api/logout') if (this.socket) { this.socket.close() } this.session = false }, async getSession() { this.session = await this.$axios.$get('/api/session') this.dialog = true }, connectWs() { this.socket = io({ transports: ['websocket'] }) this.socket.on('connect_error', (data) => { this.socketStatus = data.message }) this.socket.on('echo', (data) => { this.socketStatus = data.message }) this.socket.on('disconnect', (reason) => { this.socketStatus = 'disconnected' if (reason === 'io server disconnect') { this.socket.connect() } }) }, }, } </script> |
検証用プログラムは以下
参考リンク
- https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
- http://www.passportjs.org/docs/downloads/html/
- https://nuxtjs.org/ja/docs/features/data-fetching/#async-data
コメント