【Node.js】Express + MySQLでJWTトークン認証を実装する

nodejs jwt token

JWTとは?

JWT(JSON Web Tokenの略称でジョットと呼びます)は電子署名付きの URL-safe(URLに利用出来る文字だけ構成される)JSONデータのことです。基本的に下記の性質を持っています。

  • 署名時に鍵を用いることで、サーバー側で改ざんが行なわれていないかをチェックすることができる。
  • 「基本的に」JSONを手に入れれば、誰でも見ることができる。 ( JWTのデバッグツール )

今回はJWTを利用して、下記のような認証手順を Node.js で簡易的に行ってみたいと思います。

  • 1: ユーザーが、認証のために必要な情報 (名前 + パスワード) をサーバーに送信する。
  • 2: サーバーで認証情報が正しいと判定されたときに、ユーザーに対して有効期限付きのJSONトークン (JWT) を生成する。この生成のとき、電子署名に鍵を使うことで本当にサーバーが発行した正しいトークンかどうか? を検証することができる (JSONの改ざん対策)。そして、サーバー側は生成したこのトークンをユーザー側に発行する。
  • 3: ユーザー側はこのJWTをlocalStorageなど任意の場所に保存する。ユーザーが認証に必要なAPIを叩くときには、サーバー側から発行されたトークンをヘッダにつけて、サーバーー側に送信する。
  • 4: サーバー側は発行されたJWTトークンが正しければ、認証を通過させる。改ざんされていたり、有効期限が切れていた場合には認証を通さない。

Node.jsのJWTライブラリとして jsonwebtoken を使用します。

実装したサンプル

サンプル

  • サンプルの動かし方

localhsotでMySQLが動作している必要と 'sample' というデータベースが作成されている必要があります。必要であれば、 index.ts の下記のコードを編集してください。

/* localhostの3306ポートで動作しているMySQLのsampleというデータベースにrootユーザーのパスワードなしで接続する */
const sequelize = new Sequelize('sample', 'root', null, {
    host: 'localhost',
    dialect: 'mysql',
    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
    },
    operatorsAliases: Op
});

ライブラリーのインストールとビルド。Node.js サーバーの起動。

yarn
yarn run build
node app/index.js

Node.jsサーバーを立ち上げてから、ブラウザでindex.html を開くことで動作します。

使用ライブラリ

  • TypeScript 2.8.1
  • Node.js v.8.11.1
  • Express v.4.16.3
  • sequelize v.4.37.6
  • jsonwebtoken v.8.2.1
  • mysql2 v.1.5.3

サーバー側 (TypeScriptで記述しています)

import {Express, Request, Response, NextFunction} from "express";
const express = require("express");
const app: Express = express();
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const APP_SECRET = 'himitsu';
const crypto = require('crypto')

/* MySQLのsampleというデータベースにrootユーザーのパスワードなしで接続 */
const sequelize = new Sequelize('sample', 'root', null, {
    host: 'localhost',
    dialect: 'mysql',
    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
    },
    operatorsAliases: Op
});

/* 簡易ユーザーモデルを作成する。パスワード暗号化はなし。 */
const User = sequelize.define('user', {
    name: {
        type: Sequelize.STRING,
        unique: true,
        null: false
    },
    password: {
        type: Sequelize.STRING,
        null: false
    },
    hash: {
        type: Sequelize.STRING,
        unique: true,
        null: false
    }
});

(async () => {
    await sequelize.sync();
    User.findOrCreate({where: {name: 'hello'}, defaults: {password: 'pass', hash: crypto.randomBytes(8).toString('hex')}});
})();

/* JSONのリクエストを処理できるようにする */
app.use(bodyParser.json());


/* ヘッダ群を許可する。 */
app.use((req: Request, res: Response, next: NextFunction) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
    next();
});

/* トークンを生成する */
const generateToken = (userHash: string) => (
    jwt.sign(
        {hash: userHash},
        APP_SECRET,
        {
            algorithm: "HS256",
            expiresIn: 60 * 60 * 24
        }
    )
)

/* トークンが正しいか検証する。 */
const verifyToken = (token: string) => (
    new Promise((resolve, reject) => {
        jwt.verify(token, APP_SECRET, (err: any, decoded: string) => {
            if (err) {
                reject(err);
            }
            resolve();
        });
    })
);

/* Ajaxリクエストのみを通す */
const xhrOnly = (req: Request, res: Response, next: NextFunction) => {
    if(!(req.xhr)) {
        res.sendStatus(404)
    }

    next();
}

/* アクセスにJWTトークンを要求する */
const requireToken = async (req: Request, res: Response, next: NextFunction) => {
    if (!req.headers) {
        res.sendStatus(401);
    }

    if (req.headers.authorization) {
        const token = req.headers.authorization.replace(/Bearer\s/, '');
        await verifyToken(token).catch((error: any) => {
            console.error(error);
            res.sendStatus(401);
        });

        next();
    }

    res.sendStatus(401);
}

/* ユーザー認証 JWT発行 */
app.post('/', async (req: Request, res: Response, next: NextFunction) => {
    const user = await User.findOne({where: {name: req.body.name}}).catch((error: any) => {
        console.error(error);
        return res.sendStatus(500);
    });

    return user && req.body.password === user.password ? res.send(generateToken(user.hash)) : res.sendStatus(400);
});

/* トークン要求API */
app.get('/secret', xhrOnly, requireToken, async (req: Request, res: Response, next: NextFunction) => {
    res.send('secret data!');
});

const server = app.listen(3000, () => {
    console.log("Node.js is listening to PORT:" + server.address().port);
});

フロント側 (ES5で記述しています。)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
</head>
<body>
<div>
    <label for="name ">Name</label>
    <input type="text" class="js-name" id="name "></label>
    <label for="pass">Password</label>
    <input type="password" class="js-pass" id="pass" />
    <button class="js-submit">トークンセット</button>
</div>
<br/>

<div>
    <button class="js-secret-submit">トークン要求APIを叩く</button>
</div>

<div>
    <button class="js-clear-token">トークンをクリア</button>
</div>

<script>
    document.querySelector('.js-submit').addEventListener('click', function () {
        var name = document.querySelector('.js-name').value;
        var password = document.querySelector('.js-pass').value;

        axios.post('http://localhost:3000', {
            name: name,
            password: password
        })
            .then(function (response) {
                localStorage.setItem('token', response.data);
                console.log('トークン: ' + response.data + ' がセットされました。');
                alert('トークン: ' + response.data + ' がセットされました。');
            })
            .catch(function (error) {
                alert(error);
            });
    });

    document.querySelector('.js-secret-submit').addEventListener('click', function () {
        axios.post('http://localhost:3000/secret', {name: name}, {headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token')}})
            .then(function (response) {
                alert(response.data);
            })
            .catch(function (error) {
                alert(error);
            });
    });


    document.querySelector('.js-clear-token').addEventListener('click', () => {
        localStorage.removeItem('token');
        alert('トークンをクリアしました。')
    })

</script>
</body>
</html>

動作イメージ

Node.jsサーバーを立ち上げたときに自動で、下記のユーザーが作成され、MySQLにInsertされます。

name: hello
password: pass

index.htmlを開いて、上記認証情報をセットして Set TokenするとトークンがLocalStorageにセットされます。 すると、認証を要求されるAPIを叩くことができるようになります。

Node.js JWT authentication
  • このエントリーをはてなブックマークに追加