LoginSignup
14
12

More than 5 years have passed since last update.

JSON Web Tokenを使ってSocket.IOで認証する

Last updated at Posted at 2015-12-20

Socket.IOでJWTでの認証ってどうやったらできるのかなーと思って基本的な動作をするコードを書いてみました。

結論から言えば問題なく動作しました。
コードはGitHubに置いてあるので、もっといい書き方があればコメントかPRください。
sample-angular-express-socketio-jwt

環境

以下の環境で実施

  • Node 4.2.2
  • Express 4.13.1
  • Socket.IO 1.3.7
  • jsonwebtoken 5.4.1
  • socketio-jwt 4.3.3
  • AngularJS 1.4.0
  • ngstorage 0.3.10
  • angular-socket-io 0.7.0

server以下にNode(Express+Socket.IO)プロジェクトが、front以下にAngularプロジェクトがあります。

server

$ npm start

で動かします。3080番ポートでリッスンします。

front

$ gulp serve

で動かします。3000番ポートでリッスンします。

gulpのレシピをいじれば3080番ポートにプロキシできますが、今回はあえて直接APIサーバーにアクセスさせてます。
その場合CORS(Cross-Origin Resource Sharing)が必要になりますがモジュールを導入して解決しています。

また練習も兼ねてCoffeeScriptで書いてみました。

サーバーサイド

express-generatorをベースに開発。

$ npm install -g express-generator
$ express --git server

以下、serverディレクトリで作業。

起動時のPORT指定

package.json
{
  "scripts": {
    "start": "PORT=3080 node ./bin/www"
  },

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

$ npm install --save coffee-script cors jsonwebtoken socket.io socketio-jwt

JWT用の共通鍵を定義

config.coffee
module.exports = exports =
  secret: "your project secret key"

認証用APIを定義

今回はusername=madokapassword=magicaのみ認証を通してそれ以外は弾くようにしています。
実際にはDBに接続することになるはずです。
あと有効期限を2時間にしてるのは気分です。

routes/api.coffee
jwt = require "jsonwebtoken"
express = require "express"

config = require "../config"

router = express.Router()

router.post "/authenticate", (req, res, next) ->
  user =
    username: req.body.username
    password: req.body.password

  if user.username == "madoka" and user.password == "magica"
    token = jwt.sign user, config.secret, expiresIn: "2h"
    user.token = token

    res.json(user)
  else
    res.sendStatus 401


module.exports = router

Expressの設定を適宜追加

今回はフロントサーバーとAPI+Socketサーバーがクロスドメインとなるため、普通だとAjax通信ができません。
JSONPとかを使えば抜けられるもののGETしか対応できないので、今回はAPI+Socketサーバーの方でCORSを実装することでクロスドメインの問題を解決します。
自分でヘッダーを設定してもいいんですが、corsモジュール使えば一発OKらしいので楽をします。

app.coffee
# load cors
cors = require "cors"

# load api
api = require "./routes/api"

# use cors
app.use cors()

# use api
app.use "/api", api

Socket.IOの処理を定義

Socket.IOはコネクション確立にフックする仕組みを提供しています。
クッキーとかでやりとりする場合でも対応できます。
今回はJWTのトークンをチェックすることで認証を行いますが、これもモジュールがあるのでそれを使います。
トークンに問題がなければsocket.decoded_token経由でデータにアクセスが可能です。

socketio/app.coffee
socketioJwt = require "socketio-jwt"
config = require "../config"

module.exports = exports = (server) ->
  io = require("socket.io") server
  io.use socketioJwt.authorize
    secret: config.secret
    handshake: true

  io.on "connection", (socket) ->
    console.log socket.decoded_token
    socket.on "ping", (data, fn) ->
      fn "pong: #{data}"

Socket.IOとExpressを連携

引数のserverはhttpオブジェクトです。

bin/www
/**
 * Socket.IO
 */

require('../socketio/app')(server);

最後に、JavaScriptではなくCoffeeScriptで書いているので、それ用の設定を追加します(先頭の方に)

bin/www
require('coffee-script/register');

クライアントサイド

gulp-angularをベースに開発。

$ npm install -g yo gulp generator-gulp-angular bower
$ mkdir front && cd $_ && yo gulp-angular

以下、frontディレクトリで作業。

必要なライブラリをインストール

$ bower install --save ngstorage angular-socket.io socket.io.client

ロードするように設定

src/app/index.module.coffee
angular.module 'front',
  ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngResource', 'ngStorage', 'ui.router', 'ui.bootstrap', 'btford.socket-io']

Userモデル(サービス)を定義

認証して発行されたトークンはローカルストレージに保存しておく。

src/app/service/user.service.coffee
class User
  constructor: (@$localStorage, @$http) ->
    @load()

  authentication: ->
    @$http.post "http://localhost:3080/api/authenticate",
        username: @username
        password: @password
      .success (data) =>
        @$localStorage.user = data
        @load()

  discard: ->
    delete @$localStorage.user.token
    @token = null

  load: ->
    angular.extend(@, @$localStorage.user)

angular.module "front"
  .service "User", User

socketServiceを定義

サーバーと接続するSocketをサービスとして定義する。
Promiseを利用して、サービスを使う側で認証成功/失敗時の処理をかけるようにしておく。
また接続時にqueryで発行済みのトークンを渡すことによって、Socketサーバーで認証のチェックを行えるようにする。

また"force new connection": trueを指定してるのは、SPAの場合コネクションを勝手に貼り直しはせず内部でプーリングしてそれを使いまわそうとするらしく、強制的に新規接続をさせている。
そうしないとログイン/ログアウトを繰り返した際に、認証のチェックがうまく働かない。

src/app/service/socket.service.coffee
class SokcetService
  constructor: (@socketFactory, @$q) ->

  connectWithAuth: (token) ->
    deferred = @$q.defer()

    conn = io.connect ":3080",
      query: "token=#{token}"
      "force new connection": true

    conn.on "connect", ->
      deferred.resolve(socket)
    .on "error", (err) ->
      deferred.reject(err)

    socket = @socketFactory
      ioSocket: conn

    return deferred.promise

angular.module "front"
  .service "socketService", SokcetService

ログイン画面

ログイン画面を作ります。

src/app/login/login.controller.coffee
class LoginController
  constructor: (@User, @$state) ->

  auth: ->
    @User.authentication()
      .success => @$state.go "main"

angular.module "front"
  .controller "LoginController", LoginController
src/app/login/login.jade
div.container
  form
    input(ng-model="login.User.username")
    input(type="password", ng-model="login.User.password")
    button(ng-click="login.auth()") ログイン

メイン画面

ログイン後の画面を作ります。
$scope.$on "destroy"で処理をしているのはやはりSPAの宿命なのか、画面遷移をしてコントローラーが再作成されたりするとイベントリスナーだけゾンビのように生きていて多重に処理が走るため、それの対応です。
またSocketのコネクションも残ってしまうので明示的に切断しています。

src/app/main/main.controller.coffee
class MainController
  results = []
  count = 0

  constructor: (@User, @socket, @$state, $scope) ->
    $scope.$on "$destroy", =>
      @socket.disconnect()
      @socket.removeAllListeners()
    @results = results

  ping: ->
    @socket.emit "ping", "test!!", (data) ->
      results.push {count: ++count, data: data}

  logout: ->
    @User.discard()
    @$state.go "login"

angular.module "front"
  .controller "MainController", MainController
src/app/main/main.jade
div.container
  p main
  button(ng-click="main.ping()") ping
  button(ng-click="main.logout()") ログアウト

  ul: li(ng-repeat="data in main.results | orderBy:'':-1") {{data.count}}:{{data.data}}

ルーティング

最後にルーティングの設定をします。
メイン画面はUI Routerのresolveを利用し、遷移前に接続を確立させています。

src/app/index.route.coffee
angular.module "front"
  .config ($stateProvider, $urlRouterProvider) ->
    $stateProvider
      .state "login",
        url: "/"
        templateUrl: "app/login/login.html"
        controller: "LoginController"
        controllerAs: "login"
      .state "main",
        url: "/main"
        templateUrl: "app/main/main.html"
        controller: "MainController"
        controllerAs: "main"
        resolve:
          socket: (socketService, User, $state) ->
            socketService.connectWithAuth User.token
              .then (socket) ->
                console.log "success!"
                return socket
              , (err) ->
                if err.type == "UnauthorizedError" or err.code == "invalid_token"
                  console.log "failed!!"
                  $state.go "login"

    $urlRouterProvider.otherwise '/'

まとめ

基本的にはモジュールを組み合わせれば大丈夫ですね。
今回はAngularJS + Express + JWTという組み合わせに、Socket.IOを組み込むことで、SPAでSocket.IOに認証機能を持たせることができました。

結構手探りな部分が多かったので、運用実績のある構成やここはこうした方がいいよみたいな指摘等あればぜひコメントなどでいただければと思います。

sample-angular-express-socketio-jwt

14
12
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
14
12