本文書では、Express.jsを使ってJSON認証サーバの構築方法の説明を行っています。ユーザ情報を保存するデータベースにはSQLiteを利用しています。またGETリクエストやPOSTリクエストはVisual Studio CodeのRest Clientを利用しています。

フロントエンド側にはVue.jsのフレームワークNuxt.jsを使うことを前提に記述しているため下記の文書をあわせて読むとNuxt.jsでのユーザ認証機能の実装方法の基礎を理解することができます。

実装する機能は下記の通りです。

  • URL:/api/auth/registerにユーザ登録のPOSTリクエストがあった場合、データベースSQLiteのusersテーブルにユーザ情報を保存する
  • URL:/api/auth/loginにPOSTリクエストがあった場合、送られてくるユーザ情報のチェックを行い、登録済みのユーザである場合はTOKEN情報を返す
  • URL:/api/auth/userにGETリクエストがあった場合ヘッダーに保存されているTOKENの確認を行い、問題がなれればユーザ情報を戻す

環境設定

Express.jsのインストール

Express.jsインストール用のディレクトリbackendを作成します。ディレクトリ名は任意なので適当な名前をつけてください。作成後、backendディレクトリに移動します。


 $ mkdir backend
 $ cd backend

パッケージ管理を行うためにpackage.jsonファイルを作成します。ファイルの作成はnpm init -yコマンドで行います。


 $ npm init -y
npm initコマンドでは対話式でインストールを行いますが、-yオプションをつけると対話式をスキップしてデフォルト設定でpackage.jsonファイルが作成されます。

package.jsonファイル作成後、npm installコマンドを使ってexpressパッケージのインストールを行います。


 $ npm install express

Express.jsの動作確認

Express.jsのインストールが正常に完了しているか確認するために動作確認を行います。

backendディレクトリにindex.jsファイルを作成して下記のコードを記述します。ポートを5000に設定し、”/”(ルート)にアクセスがあるとHello Worldを戻すだけのシンプルなコードです。


const express = require('express')
const app = express()
const port = 5000

app.get('/', (request, response) => response.send('Hello World!!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

index.jsファイルにコードを記述後は下記のコマンドを実行してください。


 $ node index.js 
Example app listening on port 5000!

ブラウザからhttp://localhost:5000にアクセスするとブラウザ上にHello World!!が表示されていることを確認してください。表示されれがExpress.jsの動作確認を完了です。

nodemonのインストール

Express.jsのコードを記述するindex.jsファイルのファイル更新を監視するためにnodemonパッケージをインストールします。index.jsファイルの更新を行うとnodemonが自動で更新を反映してくれます。必須ではありませんが効率的に動作確認を行うことができます。


 $ npm install nodemon

先程は動作確認のためnode index.jsコマンドを実行しましたが、nodemonを実行後は以下のコマンドを実行しておきます。このコマンドの実行によりnodemonがindex.jsファイルの更新を監視し自動で反映してくれるようになります。


$ npx nodemon index.js
[nodemon] 2.0.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Example app listening on port 5000!

SQLite用のパッケージをインストール

Sqlite3データベースをExpress.jsから利用するためのパッケージをインストールします。


 $ npm install sqlite3

データベースの設定

SQLiteのインストールが行われていない場合は先にSQLiteのインストールを行ってください。Macではデフォルトからインストールされています。

SQLiteのデータベースの作成

SQLiteのデータベースの作成を行います。backendディレクトリで実行します。


 $ mkdir database
 $ touch database/database.sqlite3

Express.jsから作成したデータベースに接続できるかの確認を行います。


const express = require('express')
const app = express()
const port = 5000

const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database('./database/database.sqlite3', (err) => {
    if (err) {
      return console.error(err.message);
    }
    console.log('Connected to the SQlite database.');
  });

app.get('/', (request, response) => response.send('Hello World!!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

nodemonを実行しているコンソールにはデータベースへの接続が完了したメッセージが表示されます。


[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Example app listening on port 5000!
Connected to the SQlite database.

データベースの接続が完了できたのでusersテーブルの作成を行います。nodemonで監視している状態で下記のコードをindex.jsファイルの下部に追加します。保存した瞬間にnodemonが変更を検知してテーブル作成のコードを実行します。ターミナルにtable createdが表示されたらテーブルの作成は完了です。テーブルの作成後ははindex.jsファイルからテーブル作成のコードは削除してください。


let sql = `CREATE TABLE USERS(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    email TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL
    )`

db.run(sql,(err) => {
    if(err) {
        return console.error(err.message);
    }
        console.log('table created');
    }
 )
テーブルが作成済みの場合はtable USERS already exitestのエラーが表示されます。

SQLiteデータベースの中身を確認するためにデータベース管理ソフトウェアのTablePlusを利用します。

接続が完了するとusersテーブルが作成されていることを確認することができます。

テーブルの作成
テーブルの作成
usersテーブルの作成はコードからではなくTablePlusから直接行うこともできます。

ユーザの登録

bcryptパッケージのインストール

データベースに保存するユーザのパスワードはハッシュ化して保存するためにbcryptパッケージのインストールを行います。


npm install bcrypt

インストール後、index.jsファイル内でbcryptをrequireで読み込みsaltRoundsの設定を行います。bcryptではsaltRoundsを使って何回ハッシュ化を行うのか設定を行います。10はnの10乗を意味するので1024回のハッシュ化を行います。


const bcrypt = require('bcrypt')
const saltRounds = 10

ユーザの登録

作成したデータベースにユーザの登録が行えるのかどうか動作確認を行います。

下記のコードを一時的にdb接続のコードの下に記述してください。ユーザ名はjohndoe, メールアドレスはjohn@example.com、パスワードはpasswordに設定を行っています。


const insert = 'INSERT INTO USERS (name, email, password) VALUES (?,?,?)'

bcrypt.hash("password", saltRounds, (err, hash) => {
    db.run(insert, ["johndoe","john@example.com",hash])
})

TablePlusで確認するとユーザが登録されていることが確認できます。またパスワードもハッシュ化されていることも確認できます。

テーブルへのユーザ登録
テーブルへのユーザ登録
ユーザ作成後、ユーザ作成のコードはindex.jsファイルから削除しておいてください。

作成したユーザ一覧情報の取得

エディターにVisual Studio Codeを利用している場合は、拡張機能REST clientをインストールすると簡単にHTTPリクエストでExpressサーバにアクセスすることが可能です。

REST clientのインストール
REST clientのインストール
エディターにVisual Studio Codeを利用していない人はPostmanなどの他のツールで動作確認を行ってください。

インストールが完了したら拡張子はrestかhttpで任意のファイルを作成してください。ここではtest.httpという名前でファイルを作成しています。

test.httpファイルを作成します。最初にExpressサーバにアクセスできるか確認を行います。


GET http://localhost:5000/

test.httpに上記のGETの一行を追加すると上部にSend Requestと表示されるのでクリックするとHTTPのGETリクエストが行われます。画面の右側にサーバからのレスポンスが表示されます。Hello World!!が表示されたらHTTPのGETリクエストからのレスポンスが正常に戻されていることがわかります。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Thu, 19 Dec 2019 10:26:47 GMT
Connection: close

Hello World!!

次にusersテーブルに保存されているユーザ情報の一覧を取得できるようにルーティングを追加します。


app.get("/api/users", (req, res, next) => {
    const sql = "select * from users"
    const params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
          return res.status(400).json({"error":err.message})
        }
        return res.json({
            "message":"success",
            "data":rows
        })
      });
});

test.httpにGETリクエストを追加してユーザ一覧を取得します。


GET http://localhost:5000/api/users

登録しているユーザが一人の場合は下記のように表示されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 157
ETag: W/"9d-S4yccX3DITwj6YLczrBkdpUove4"
Date: Tue, 24 Dec 2019 00:25:25 GMT
Connection: close

{
  "message": "success",
  "data": [
    {
      "id": 3,
      "name": "johndoe",
      "email": "john@example.com",
      "password": "$2b$10$GauvurvWO6w8jVKAR5LJBuX9l0tzlWZpenboCGkKrf460lyescfbO"
    }
  ]
}

エラーの場合の動作確認も行っておきます。index.jsファイル内のテーブルの名前をusersからuserに変更した場合は下記のメッセージが表示されます。


HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 45
ETag: W/"2d-UO1+fW2HQ3gbb8Zjpc1L/WYjr7U"
Date: Tue, 24 Dec 2019 00:33:13 GMT
Connection: close

{
  "error": "SQLITE_ERROR: no such table: user"
}

POSTリクエストによるユーザ登録

HTTPのPOSTリクエストでユーザ登録ができるようにコードの追加を行います。

フロントエンドにユーザ登録フォームを作成し、入力した情報をPOSTリクエストで送信することでユーザの作成を行うことができます。

ユーザを登録するルーティングを追加します。先程のユーザ登録では直接ユーザ名、パスワード、メールアドレスを追加していましたが、今回はPOSTリクエストでそれらの情報を受け取る必要があります。


app.post('/api/auth/register/', (req, res) => {
  const insert = 'INSERT INTO USERS (name, email, password) VALUES (?,?,?)'
  bcrypt.hash(req.body.password, saltRounds, (err, hash) => {
    db.run(insert, [req.body.name,req.body.email,hash],(err) => {
      if (err) {
        return res.status(400).json({"error":err.message});
      }
      return res.json({
        "message": "create User successfully",
        "data": [req.body.name, req.body.email]
      })
    })
  })
})

test.httpにPOSTリクエストを記述しユーザの登録ができるか確認を行います。


POST http://127.0.0.1:5000/api/auth/register/
Content-Type: application/json

{
    "name": "kevin",
    "email": "kevin@test.com",
    "password": "password"
}
Content-Typeの下の空行やPOSTリクエストの下に空行が入る場合はリクエストがうまく動作しないので空白にも注意してください。

実行するとInternal Server Error 500が発生して以下のメッセージが表示されます。これは受け取ったPOSTリクエストの内容をExpressサーバ側で取り出すことができないためです。


TypeError: Cannot read property 'password' of undefined

POSTリクエストから送られてきた内容を取り出すために下記を追加します。


app.use(express.json())

追加後、再度POSTリクエストを送ると登録したユーザのnameとemailが戻されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 74
ETag: W/"4a-cyVYFjxnENRXEBo4NRqtTlqprm8"
Date: Thu, 19 Dec 2019 11:11:41 GMT
Connection: close

{
  "message": "create User successfully",
  "data": [
    "kevin",
    "kevin@test.com"
  ]
}

再度同じメールアドレスでユーザを登録しようとするとエラーメッセージが表示されます。


HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 68
ETag: W/"44-ffAuTeqwiyGjM74XB3CH5FnCVcE"
Date: Tue, 24 Dec 2019 00:50:14 GMT
Connection: close

{
  "error": "SQLITE_CONSTRAINT: UNIQUE constraint failed: USERS.email"
}

ユーザの認証

ユーザのパスワードチェック

登録したユーザのメールアドレスとパスワードを使ってPOSTリクエストを送ると送ったユーザ情報とデーターベースに保存されているユーザ情報が一致するかチェックする機能を追加します。

ここでの設定はフロントエンド側のログインフォームから送られてくるメールアドレスとパスワードを使った認証を行う部分で利用します。

POSTリクエストに含まれるメールアドレスがusersテーブルに存在しているのか確認を行い、存在する場合はユーザ情報を取り出し(user)、取り出したパスワードとPOSTリクエストで送られてきたパスワードが一致するか確認しています。

パスワードのチェックには、bcrypt.compareを利用しています。bcrypt.compareではPOSTリクエストに含まれるパスワードとデータベースから取り出したパスワードを比較しています。データベースから取り出したパスワードがハッシュ化されているのでbcrypt.compareが必要になります。


app.post('/api/auth/login/',(req,res) => {
  const sql = 'select * from users where email = ?'
  const params = [req.body.email]
  db.get(sql, params, (err, user) => {
    if (err) {
      return res.status(400).json({"error":err.message});
    }
    if(!user){
      return res.json({"message": "email not found"})
    }
    bcrypt.compare(req.body.password, user.password, (err,result) => {
      if (err) {
        return res.status(400).json({"error":err.message});
      }
      if (!result) {
        return res.json({"message" : "password is not correct"})
      }
      return res.json({"message" : "password is correct"})
    })
  })
})

ここまででユーザの登録とメールアドレスをパスワードのチェックを行う機能(ユーザ認証)の追加が完了しました。

JWTの設定

JSON WEB TOKENのインストール

ユーザ登録後、ログインに成功した場合のみTokenを渡します。Tokenの処理はjsonwebtokenパッケージを利用します。

npm installコマンドでjsonwebtokenのインストールを行います。


 $ npm install jsonwebtoken

インストールしたjsonwebtokenをrequireで読み込みます。


const jwt = require('jsonwebtoken')

JWTのTokenの作成

メールアドレスとパスワードによる認証が正常に完了した場合のみTokenを渡すため、index.jsのパスワード認証のresponseを更新する必要があります。

Tokenを作成するためには最低payloadとキーが必要になります。payloadにはTokenに含めたい情報を設定します。jwt.signの2番目の引数にはハッシュ化に利用するキーを設定します。このキーの値は任意です。ここではsecretを設定しています。


// return res.json({"message" : "password is correct"})

      const payload = {
                        id: user.id,
                        name: user.name,
                        email: user.email
                      }

      const token = jwt.sign(payload,'secret')
      return res.json({token})

test.httpを利用して正しい認証情報のメールアドレスとパスワードを使ってPOSTリクエストを行うとTokenを入手することができます。リクエスト毎にTokenの値が変わることも確認しておいてください。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 180
ETag: W/"b4-DhTH7IJRXOQ7HXgYx++V8c9qgjk"
Date: Tue, 24 Dec 2019 02:13:20 GMT
Connection: close

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibmFtZSI6ImtldmluIiwiZW1haWwiOiJrZXZpbkB0ZXN0LmNvbSIsImlhdCI6MTU3NzE1MzYwMH0.KxpFpgT7_Nq9gCtZiLtGe9GxjVvc-pyCsiW-AA3WWHI"
}

Tokenに含まれるpayloadは暗号化されているわけではないので、jwt.ioのdebuggerを利用するとpayloadの中身を確認することができます。Tokenの値がわかれば誰でもpayloadの中身を確認することできます。

JWT TOKENの確認
JWT TOKENの確認

Tokenの確認

Tokenをブラウザに渡した後、ブラウザがアクセスしてきた場合にブラウザから送られてくるTokenが正しいのかチェックを行う必要があります。

新たに/api/auth/userのルーティングを追加します。下記の処理ではGETリクエストのヘッダーに入っているauthorizationからTokenを取り出しています。Tokenはauthorizationの中でBearer XXXX(Token)の形式で入っています。

jwt.verifyには取り出したTokenとToken作成時に利用したキー”secret”を利用して確認を行っています。正しい場合はTokenに含まれていたpayloadを戻しています。


app.get('/api/auth/user/',(req,res) => {

  const bearToken = req.headers['authorization']
  const bearer = bearToken.split(' ')
  const token = bearer[1]

  jwt.verify(token,'secret',(err,user)=>{
    if(err){
      return res.sendStatus(403)
    }else{
      return res.json({
            user
          });
    }
  })
});

test.httpリクエストで取得したTokenをAuthorizationに設定してTokenの認証を行います。


GET http://127.0.0.1:5000/api/auth/user/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibmFtZSI6ImtldmluIiwiZW1haWwiOiJrZXZpbkB0ZXN0LmNvbSIsImlhdCI6MTU3NzE1NDA4Nn0.uSSn6Q8IGK1xL2EcsxX3dX_3XZIuuh8lZ4UxzShZg34

Tokenが正しい場合は、以下のようにpayloadに設定したユーザの情報を取得することができます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 74
ETag: W/"4a-oAIn9TG3QKRcx95jUd6l7N5K484"
Date: Tue, 24 Dec 2019 02:40:12 GMT
Connection: close

{
  "user": {
    "id": 5,
    "name": "kevin",
    "email": "kevin@test.com",
    "iat": 1577154086
  }
}

もしTokenに誤りがある場合は下記のようにForbiddenが表示されます。


HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 9
ETag: W/"9-PatfYBLj4Um1qTm5zrukoLhNyPU"
Date: Tue, 24 Dec 2019 02:41:07 GMT
Connection: close

Forbidden

本文書ではSQLiteを利用していましたがユーザ情報を保存することができればSQLiteである必要はありません。

実際に利用する場合はセキュリティについて考える必要がありますが、本文書を通してExpress.jsにおけるJWTの設定方法の流れを理解することができたと思います。

本文書ではフロントエンド側について全く触れていないので、フロントエンドを含めて全体の設定を理解したいと思います。フロントエンドについてはNuxt.jsを利用して動作確認を行っているので下記の文書を参考にしてください。