Node.js(Express)とJWTでVue3ダッシュボードにユーザ認証機能を実装
本文書は公開済みの”Vue3のComposition API(script setup)でダッシュボードレイアウト作成”の続きです。作成したダッシュボードにJWT(JSON WEB TOKEN)を利用した認証機能を追加し、ログイン画面で認証が完了したユーザのみダッシュボードを含めたページにアクセスできるように設定を行なっていきます。
ユーザの認証を行うバックエンドサーバにはNode.jsのWEBフレームワークのExpress.jsを利用します。ユーザ情報を含め各種情報はデータベースのMongoDBを利用して保存します。ユーザ認証にはJWT(JSON WEB TOKEN)を利用します。仕組みはシンプルでログイン認証が完了したユーザにサーバで生成したTokenを渡しアクセスする度にTokenをチェックすることで正しいTokenを持つユーザのみサーバが管理するリソースへのアクセスが可能となります。
Express.js、MongoDBの設定はダッシュボードを構築しているフロント側の影響を受けないのでVue3で作成したダッシュボード以外にも利用することができます。
目次
Express.jsの構築
Express.jsに必要なパッケージのインストール
Express.js用に任意のフォルダbackendを作成します。作成後npm init -yコマンドを実行するとbackendフォルダにpackage.jsonファイルが作成されます。
% npm init -y
package.jsonファイル作成後にバックエンドサーバの構築に必要なパッケージをインストールします。各パッケージの説明については設定を行う際に説明を行っていきます。最初にインストールするパッケージはexpress, dotenv, mongoose, nodemonです。これ以外のパッケージについても必要がある場合は追加していきます。
% npm install express dotenv mongoose nodemon
Express.jsの起動
Express.jsのメインファイルであるindex.jsと.envファイルを作成します。index.jsは任意の名前です。dotenvモジュールを利用しているので.envファイルに環境変数を設定することができます。.envファイルに記述した環境変数はExpress.jsからアクセスすることが可能でデータベースの接続に必要な接続情報などを保存します。
Express.jsの起動のポートを.envファイルから変更できるようにPORTの設定を行います。
PORT = 5000
index.jsファイルにExpress.jsの起動コードを記述します。
const express = require("express");
const dotenv = require("dotenv");
dotenv.config();
const app = express();
app.listen(process.env.PORT || 5000, () => {
console.log(`Backend server is runnnig port number ${process.env.PORT}`);
});
Expressサーバを起動するためにpackage.jsonファイルのscriptsにstartを追加します。nodemonを利用することでindex.jsファイルを更新した場合にnodemonが更新を検知してindex.jsファイルの再読み込みを行います。nodemonがない場合は更新を行う度にindex.jsを再読み込みするためにコマンドを毎回手動で実行する必要があります。
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^6.0.12",
"nodemon": "^2.0.14"
}
}
npm run startコマンドでExpress.jsを起動します。起動したコンソールにindex.jsに記述した起動に関するメッセージが表示されることを確認してください。
% npm run start
//略
[nodemon] starting `node index.js`
Backend server is runnnig port number 5000
MongoDBへの接続
データベースにはMongoDBを利用しますが本文書ではクラウドのMongoDB Cloudを利用します。これまでにクラウドのMongoDBの設定を行なったことがない人は下記の文書を参考にデータベースの設定を行ってください。
接続情報についてはmongoDBのCloudの管理画面のDatabaseのConnectをクリックすると接続方法の情報が表示されるので真ん中の”Connect your Application”を選択します。
“Connect your application”をクリックして表示された画面の2にアプリケーションで利用するコードが表示されるのでこのコードを利用します。usernameとpasswordには各自が設定した情報を入力します。
.envファイルを開いてMONGO_URLとして追加を行います。各自の環境によって異なる値を設定します。下記の値を設定しても接続はできません。
PORT = 5000
MONGO_URL = 'mongodb+srv://username:password@cluster.tttx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
mongoDBへ接続するための関数dbConnectを追加します。Express.jsからMongoDBへの接続はmongooseを介して行うためmongooseのインポートも必要になります。
const mongoose = require("mongoose");
//略
const dbConnect = async () => {
try {
await mongoose.connect(process.env.MONGO_URL);
console.log("DB connection successfuly.");
} catch (err) {
console.log(err);
}
};
dbConnect();
mongoDBへの接続が完了したらコンソールのメッセージにDB connection successfullyが表示されればExpress.jsからMongoDBへの接続は成功しています。
% npm run start
//略
[nodemon] starting `node index.js`
Backend server is runnnig port number 5000
DB connection successfuly.
モデルの設定
Express.jsからMongoDBへのアクセスはmongosseのモデルを介して行います。MongDBにユーザ情報を保存するためのスキーマ情報をモデルファイルに追加します。スキーマでは、ドキュメントがどのような名前、スキーマタイプで構成されているかを定義することができます。name, email, passwordから構成されtypeにStringである文字列が設定されています。requiredで必須項目かどうか設定を行い、uniqueでは一意の値かどうか設定を行っています。最後の行のmongoose.modelでスキーマとモデルを紐づけています。Userというモデルを通して、MongoDBにアクセスが可能となります。
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
},
{ timestamps: true }
);
module.exports = mongoose.model("User", UserSchema);
ルーティングの設定
ユーザに関連するルーティングの作成を行います。routesフォルダを作成しその下にuser.jsファイルを作成します。ルーティング内で実行されるコードはコントローラーファイルに記述していきます。コントローラーの作成はこれから行いますがcontrollers/users.jsファイルに記述しているregisterUser関数をインポートしています。
routesフォルダを作成し、その下にuser.jsファイルを作成して以下の内容を保存します。
const express = require("express");
const router = express.Router();
const { registerUser } = require("../controllers/users.js");
router.post("/register", registerUser);
module.exports = router;
/registerに対してPOSTリクエストが送信されてくるとregisterUser関数が実行されます。
コントローラーファイルの作成
ルーティングの設定で利用しているコントローラーファイルを作成するためcontrollersフォルダを作成します。作成したcontrollersフォルダの下にusers.jsファイルを作成します。
ルーティングファイルでimportしているregisterUser関数の処理を記述します。registerUser関数ではPOSTリエクストから送信されてくるデータを元にuser情報を作成し保存しています。先ほど作成したモデルのUserを介して保存処理を行っています。
const User = require("../models/User.js");
const registerUser = async (req, res) => {
const user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password,
});
try {
const savedUser = await user.save();
res.status(201).json(savedUser);
} catch (err) {
res.status(500).json(err);
}
};
module.exports = { registerUser };
ルーティングの登録
作成したルーティング情報をExpress.jsに登録する必要があります。index.jsファイルを開いてルーティング情報を登録します。routes/user.jsファイルをuserRouteとしてimportしてapp.use(“/api/users”, userRoute)で登録しています。外部から/api/users/registerへアクセスがあるとregisterUser関数が実行されます。POSTリクエストで送信されているJSONデータを取得するためにapp.use(express.json())を設定する必要があります。
const express = require("express");
const dotenv = require("dotenv");
const mongoose = require("mongoose");
const userRoute = require("./routes/user");
dotenv.config();
const app = express();
const dbConnect = async () => {
try {
await mongoose.connect(process.env.MONGO_URL);
console.log("DB connection successfully.");
} catch (err) {
console.log(err);
}
};
dbConnect();
app.use(express.json());
app.use("/api/users", userRoute);
app.listen(process.env.PORT || 5000, () => {
console.log(`Backend server is runnnig port number ${process.env.PORT}`);
});
ユーザ情報をmongoDBに登録するためのコードが作成できたので外部からPOSTリエクストを送信してユーザから登録されるか確認します。エディターにVisual Studio Codeを利用している人は拡張機能のREST ClientをインストールしてHTTPリクエストの送信を行います。
ユーザの登録
REST Clientによるユーザの登録
動作確認を行うために拡張機能のREST Clientのインストールを行います。エディターにVisual Studio Codeを利用していない場合はPostmanなどを利用してください。この後ユーザ登録画面を作成するのでこの処理をスキップすることも可能です。
test.httpファイルを作成し、/api/users/registerにPOSTリクエストを送信します。
test
POSTリクエスト送信後にクラウド上のmongoDBの管理画面からデータを確認するとユーザが登録されていることが確認できます。
パスワードのハッシュ化
mongoDB内の保存されているデータを確認するとpasswordがそのままの形で保存されています。パスワードをハッシュ化して保存するためにbcryptを利用します。
データベースに保存するパスワードをハッシュ化するためにbcryptをインストールします。
% npm install bcrypt
ユーザ登録時にハッシュ化したパスワードをデータベースに保存します。
const bcrypt = require("bcrypt");
const registerUser = async (req, res) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(req.body.password, saltRounds);
const user = new User({
name: req.body.name,
email: req.body.email,
password: hashedPassword,
});
try {
const savedUser = await user.save();
res.status(201).json(savedUser);
} catch (err) {
res.status(500).json(err);
}
};
mongoDBの管理画面上から一度保存したusersコレクションを削除します。usersにカーソルを合わせるとゴミ箱のアイコンが表示されるのでアイコンをクリックして削除します。削除する際はコレクションの名前のusersを入力する必要があります。
削除後再度REST Clientからユーザの登録を行います。パスワードがハッシュ化されているか確認してください。
passwordという文字列から複雑な文字列にハッシュ化できていることが確認できたら次はユーザ登録画面の作成を行います。
レイアウトファイルの設定変更
これまではExpress.jsによるバックエンド側の設定を行ってきましたここではダッシュボードのレイアウトを作成したVue3のフロントエンド側の設定を行っていきます。src¥pagesフォルダにSignUp.vueファイルを作成して”ユーザ登録”を記述します。
<script setup></script>
<template>
<div>ユーザ登録</div>
</template>
SignUp.vueファイルを作成後、router¥index.jsファイルにルーティングを追加します。
//略
import SignUp from '../pages/SignUp.vue';
const routes = [
//略
{
path: '/signup',
name: 'SignUp',
component: SignUp,
},
];
/signupにブラウザからアクセスするとユーザ登録の文字列が表示されます。
SignUpコンポーネントの内容は表示されましたがユーザ登録画面はユーザのログイン前なのでダッシュボードのレイアウトが適用されている必要がありません。
App.vueファイルでDefaultコンポーネントをimportしていましたが削除を行います。
<template>
<router-view></router-view>
</template>
Defaultコンポーネントを削除したことになり/signupにアクセスすると文字列のユーザ登録のみ表示されます。/(ルート)にアクセスしてもダッシュボードのレイアウトであるDefaultが適用されていないためダッシュボードの文字列のみ表示されます。
ログイン後のページのみダッシュボードのレイアウトが適用されるようにDefaultのimportを各ページコンポーネントで行います。
<script setup>
import Default from '../layouts/Default.vue';
</script>
<template>
<Default>ダッシュボード</Default>
</template>
レイアウトのDefaultコンポーネントが適用されたダッシュボードが表示されます。
pagesフォルダ以下にあるそのほかのファイルについてもDefaultコンポーネントをimportして設定を変更してください。
ユーザ登録画面の作成
SignUp.vueファイルにユーザ登録画面を記述します。name, email, passwordのinput要素を準備し、flexboxを利用して中央に表示させています。レスポンシブにも対応できるように設定を行っています。
<template>
<div class="w-screen h-screen bg-gray-50 flex items-center justify-center">
<div class="bg-white p-16 w-full mx-16 md:mx-0 md:w-1/2 xl:w-1/3">
<h1 class="text-center border-gray-300 border-b p-4 mb-8 font-bold">
新規登録
</h1>
<form>
<div class="flex flex-col space-y-8">
<input
name="name"
placeholder="名前"
class="border p-4 rounded-sm text-sm"
/>
<input
name="email"
placeholder="メールアドレス"
class="border p-4 rounded-sm text-sm"
/>
<input
name="password"
placeholder="パスワード"
class="border p-4 rounded-sm text-sm"
type="password"
/>
</div>
<button
class="w-full mt-8 py-4 bg-blue-600 text-white text-sm rounded-sm"
>
新規登録
</button>
</form>
</div>
</div>
</template>
input要素に入力した値を保存するためrefを使ってname, email, password、エラー情報をユーザ登録画面に表示させるためにerrorを定義します。
<script setup>
import { ref } from 'vue';
const name = ref('');
const email = ref('');
const password = ref('');
const error = ref('');
</script>
input要素にv-modelを設定します。formタグも追加しています。
<form>
<div class="flex flex-col space-y-8">
<input
v-model="name"
name="name"
placeholder="名前"
class="border p-4 rounded-sm text-sm"
/>
<input
v-model="email"
name="email"
placeholder="メールアドレス"
class="border p-4 rounded-sm text-sm"
/>
<input
v-model="password"
name="password"
placeholder="パスワード"
class="border p-4 rounded-sm text-sm"
type="password"
/>
</div>
<p v-show="error" class="mt-4 text-red-500">{{ error }}</p>
<button
class="w-full mt-8 py-4 bg-blue-600 text-white text-sm rounded-sm"
>
新規登録
</button>
</form>
新規登録ボタンをクリック後にPOSTリクエストを送信できるようにsubmitイベントを設定します。submitイベントにはregister関数を指定しています。submitを実行すると通常はページのリロードが行われますがページのリロードを行わないように.preventを設定しています。
<form @submit.prevent="register">
register関数をscriptタグの中に記述します。ユーザ登録が完了したらダッシュボードのページに移動できるように設定を行います。ページの移動にはuseRouter関数を利用するためimportする必要があります。
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
//略
const router = userRouter();
//略
const register = () => {
axios
.post('http://localhost:5000/api/users/register', {
name: name.value,
email: email.value,
password: password.value,
})
.then((response) => {
console.log(response.data);
router.push('/');
})
.catch((err) => {
error.value = err.response.data.message;
});
};
POSTリクエストを送信するためにregister関数の中でaxiosを利用しているのでaxiosのインストールを行います。
% npm install axios
設定後、ユーザ登録フォームにユーザ情報を入力して”新規登録”ボタンをクリックすると処理に失敗します。デベロッパーツールのコンソールを確認するとCORSによってエラーが発生しているためPOSTリクエストを送信することができません。
Access to XMLHttpRequest at 'http://localhost:5000/api/users/register' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
バックエンドのExpress.js側でcorsのインストールを行います。
% npm install cors
インストール後にバックエンド側のindex.jsファイルでcorsの設定を行います。
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const userRoute = require('./routes/user');
dotenv.config();
const app = express();
app.use(cors());
//略
設定後再度ユーザ登録フォームに情報を入力して新規登録ボタンをクリックすると入力したユーザの情報がMongoDBに登録されることが確認できます。
またユーザ登録画面からダッシュボード画面に移動することが確認できます。またブラウザのデベロッパーツールのコンソールを見るとExpress.jsから戻されるユーザ情報を含むオブジェクトを確認することができます。
ログイン設定
Express.jsでのログインに関するルーティング、コントローラーの設定を行った後、REST Clientでログインの動作確認を行いフロントエンドのダッシュボードでログイン画面の設定を行います。
ルーティングの設定
Express.jsにログイン処理を行うルーティングの追加をroutes¥user.jsファイルで行います。新たに/loginを追加しPOSTリクエストが/loginに送信されてきた場合にloginUser関数を実行します。loginUser関数はregisterUser関数と同様にコントローラーファイルのcontrollers¥users.jsファイルに記述します。
const express = require('express');
const router = express.Router();
const { registerUser, loginUser } = require('../controllers/users.js');
router.post('/register', registerUser);
router.post('/login', loginUser);
module.exports = router;
コントローラーの設定
controllers¥users.jsファイルにloginUser関数の追加を行います。POSTリクエストで送信されてくるemailとパスワードの情報を利用してログイン処理を行います。emailを使ってfindOneメソッドでユーザ情報を取得します。ユーザが見つからない場合はエラーメッセージを戻します。パスワードはハッシュ化されているのでbrypt.compareを利用して送信されてきたパスワードと保存されているパスワードが一致しているかチェックを行います。パスワードが一致しない場合はメッセージを戻します。
//略
const loginUser = async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(401).json("メールアドレスに誤りがあります。");
const match = await bcrypt.compare(req.body.password, user.password);
if (!match) return res.status(401).json("パスワードに誤りがあります。");
res.status(200).json({
id: user._id,
name: user.name,
email: user.email,
});
};
module.exports = { registerUser, loginUser };
REST Clientによるログイン確認
REST Clientを利用してPOSTリクエストを送信して正しいemail, passwordを利用するとユーザ情報が戻されるか確認を行います。
test.httpに新たにPOSTリクエストを追加します。test.httpファイルに複数のリクエストを記述する場合は###(シャープ3個)を入力することで独立したリクエストとして認識されます。
###
POST http://localhost:5000/api/users/login
Content-Type: application/json
{
"email":"john@example.com",
"password":"password"
}
Send Requestをクリックするとemailとpasswordが正しい場合はユーザのオブジェクトが戻されます。
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 74
ETag: W/"4a-oktbS5uZAW+grvyQ5uFotzbViRE"
Date: Wed, 10 Nov 2021 01:38:14 GMT
Connection: close
{
"id": "618a1711ea27b6f100a5823a",
"name": "John",
"email": "john@example.com"
}
正しくないemailを入力した場合には”メールアドレスに誤りがあります。”が戻され、パスワードに誤りがある場合は”パスワードに誤りがあります。”が戻されます。
ログイン画面の作成
フロントエンド側でログイン画面を作成します。
src¥pagesフォルダにLogin.vueファイルを作成してログインを記述します。
<script setup></script>
<template>
<div>ログイン</div>
</template>
Login.vueファイルを作成後、router¥index.jsファイルにルーティングを追加します。
//略
import Login from '../pages/Login.vue';
const routes = [
//略
{
path: '/login',
name: 'Login',
component: Login,
},
];
http://localhost:3000/loginにアクセスするとログインの文字列が表示されます。
Login.vueファイルにログイン画面を記述していきます。CSSの設定などについてはSignUp.vueファイルとほとんど同じです。
<script setup></script>
<template>
<div class="w-screen h-screen bg-gray-50 flex items-center justify-center">
<div class="bg-white p-16 w-full mx-16 md:mx-0 md:w-1/2 xl:w-1/3">
<h1 class="text-center border-gray-300 border-b p-4 mb-8 font-bold">
ログイン
</h1>
<div class="flex flex-col space-y-8">
<input
name="email"
placeholder="メールアドレス"
class="border p-4 rounded-sm text-sm"
/>
<input
name="password"
type="password"
placeholder="パスワード"
class="border p-4 rounded-sm text-sm"
/>
</div>
<button class="w-full mt-8 py-4 bg-blue-600 text-white text-sm rounded-sm">
ログイン
</button>
</div>
</div>
</template>
input要素に入力した値を保存するためrefを使ってemail, passwordを定義します。
import { ref } from 'vue';
const email = ref('');
const password = ref('');
input要素にv-modelを設定します。formタグも追加しています。
<form>
<div class="flex flex-col space-y-8">
<input
name="email"
placeholder="メールアドレス"
class="border p-4 rounded-sm text-sm"
v-model="email"
/>
<input
name="password"
type="password"
placeholder="パスワード"
class="border p-4 rounded-sm text-sm"
v-model="password"
/>
</div>
<button
class="w-full mt-8 py-4 bg-blue-600 text-white text-sm rounded-sm"
>
ログイン
</button>
</form>
ログインボタンをクリック後にPOSTリクエストを送信できるようにsubmitイベントを設定します。submitイベントにはlogin関数を指定しています。submitを実行すると通常はページのリロードが行われますがページのリロードを行わないように.preventを設定しています。
<form @submit.prevent="login">
register関数をscriptタグの中に記述します。ログインが完了したらダッシュボードのページに移動できるように設定を行います。ユーザ登録の時と同様にuserRouterを利用しています。
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const email = ref('');
const password = ref('');
const login = () => {
axios
.post('http://localhost:5000/api/users/login', {
email: email.value,
password: password.value,
})
.then((response) => {
console.log(response.data);
router.push('/');
});
};
設定後、ログイン画面の入力フォームに登録済みユーザの情報を入力してログインボタンをクリックするとダッシュボードに移動します。誤った情報を入力した場合は画面の移動は起こりません。ブラウザのデベロッパーツールのコンソールを確認すると401 Unauthorizedを確認することができます。
POST http://localhost:5000/api/users/login 401 (Unauthorized
ログインに失敗した場合にExpress.jsから戻されるメッセージが表示されるように設定を追加します。refを利用してerrorを定義してPOSTリエクストでエラーが戻ってきた時に定義してerrorにメッセージを保存します。
const email = ref('');
const password = ref('');
const error = ref('');
const login = () => {
axios
.post('http://localhost:5000/api/users/login', {
email: email.value,
password: password.value,
})
.then((response) => {
console.log(response.data);
router.push('/');
})
.catch((err) => {
error.value = err.response.data;
});
};
保存したメッセージがログイン画面上で表示されるようにv-showディレクティブを利用して設定を行います。ログインボタンの上にpタグを追加しています。
<p v-show="error" class="mt-4 text-red-500">{{ error }}</p>
<button
class="w-full mt-8 py-4 bg-blue-600 text-white text-sm rounded-sm"
>
ログイン
</button>
メールアドレスに誤りがあった場合には下記のようにメッセージが表示されます。
ログイン画面の設定を行うことができました。
JWTによるユーザ認証
JSON WEB TOKEN(JWT)を利用してTokenを発行、検証を行うことでユーザ認証を行っていきます。Tokenの発行はバックエンドサーバ側で行います。JWTの仕組みはシンプルでExpress.jsサーバ側で発行したTokenをログイン処理が完了したユーザに戻し、ユーザが発行した正しいTokenを持っているのかTokenを検証することでユーザ認証を行います。
JWTを利用するためにjsonwebtokenのインストールをnpmコマンドで行います。
% npm install jsonwebtoken
Tokenの生成
Tokenの生成にはインストールしたjsonwebtokenからimortしたjwtのsignメソッドを利用して作成を行います。payload, secret_key, optionを設定することができ、payloadにはオブジェクト、secret_keyには任意のシークレットキー、optionにはTokenの有効期限などを設定することができます。
const jwt = require('jsonwebtoken');
jwt.sign(payload, secret_key, option);
Tokenを生成する際に利用するシークレットキーは.envファイルに保存します。
TOKEN_SECRET='secret_key'
payloadにはユーザの名前、emailやidを含めることができます。payloadの情報はTokenの中に含まれておりTokenが分かればだれでも中身を見ることができるためパスワードの情報等は含めてはいけません。後ほどフロントエンド側でTokenをデコードしてユーザ情報を取り出します。
controllers¥users.jsにTokenを生成するための関数gemerateAccessTokenを追加します。
const generateAccessToken = (user) => {
const payload = {
id: user._id,
name: user.name,
email: user.email,
};
return jwt.sign(payload, process.env.TOKEN_SECRET, { expiresIn: '1d' });
};
これまではログイン完了時にuser情報を戻していましたがuser情報を含むTokenを戻すようにloginUser関数を変更します。戻すTokenは追加したgenerateAccessTokenを利用しています。
const loginUser = async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(401).json('メールアドレスに誤りがあります。');
const match = await bcrypt.compare(req.body.password, user.password);
if (!match) return res.status(401).json('パスワードに誤りがあります。');
res.status(200).json({
token: generateAccessToken(user),
});
};
REST Clientを利用して登録済みのユーザ情報をPOSTリクエストで送信するとTokenが戻されることを確認します。
###
POST http://localhost:5000/api/users/login
Content-Type: application/json
{
"email":"john@example.com",
"password":"password"
}
Send Requestを実行するとTokenが戻されることが確認できます。
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 237
ETag: W/"ed-jZrwF8aWTQzm/WFJDMgzbi2t9MQ"
Date: Thu, 11 Nov 2021 01:14:26 GMT
Connection: close
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYxOGExNzExZWEyN2I2ZjEwMGE1ODIzYSIsIm5hbWUiOiJKb2huIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM2NTkzMjY2LCJleHAiOjE2MzY2Nzk2NjZ9.efD0FzwPxKZ4Ze-iyuG8qnlqJQUBLd8b4uyKaTtWBsY"
}
Tokenの検証設定
渡したTokenを利用してユーザからアクセスが行われた場合にそのTokenがExpress.jsで生成された正しいTokenなのかを検証する必要があります。
現在/api/users/register, /api/users/loginの2つのルーティングを追加しましたがユーザ登録、ログインが完了した場合にTokenを戻すためそれらのルーティングへのアクセス時にはTokenは必要ありません。新たに/api/users/userを追加し、正しいTokenを利用してアクセスされた場合のみユーザ情報を戻すように設定を行います。
routes¥user.jsファイルにルーティング/userを追加します。/userに対してGETリクエストがあった場合にgetUser関数を実行します。getUser関数はcontrolers¥users.jsファイルに追加します。
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, getUser } = require('../controllers/users.js');
router.post('/register', registerUser);
router.post('/login', loginUser);
router.get('/user', getUser);
module.exports = router;
登録されているユーザを検索するためにfindOneメソッドでデータベース内で一意であるemailを利用します。GETリクエストのためreq.body.emailはリクエストに含まれていません。req.body.emailと代わりになるものが必要になります。代わりとなる情報はTokenから取得します。
const getUser = async (req, res) => {
const user = await User.findOne({ email: req.body.email });
//以下はこれから追加
};
module.exports = { registerUser, loginUser, getUser };
getUser関数を実行する前にTokenの検証を行うためmiddlewareのverifyTokenを追加します。middlewaresフォルダを作成してその下にverifyToken.jsファイルを作成します。
verifyToken関数ではリクエストヘッダーからauthorizationを取得します。Tokenの情報がauthorizationに含まれている場合は”Bear eyJhbGciOiJIU….”の形をしているのでauthHeader.split(” “)[1]でBearの後ろにあるTokenのみ取得しています。jwtのverify関数を利用してTokenが正しいか検証を行い正しい場合はTokenに含まれているuser情報を取得しています。取得したuser情報をreqに設定し、vefityTokenの後に実行されるgetUser関数でuser情報を利用することができます。
const jwt = require("jsonwebtoken");
const verifyToken = (req, res, next) => {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (token == null)
return res.status(401).json("アクセストークンが含まれていません。");
jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
if (err)
return res.status(401).json("アクセストークンが有効ではありません。");
req.user = user;
next();
});
};
module.exports = { verifyToken };
作成したmiddlewareのverifyTokenを利用するためにroutes¥user.jsファイルで設定を行います。getUser関数の前にverifyTokenを追加し/userにGETリクエストが送信されてきた場合にverifyToken関数が実行され、その後getUser関数が実行されます。
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, getUser } = require('../controllers/users.js');
const { verifyToken } = require('../middlewares/verifyToken');
router.post('/register', registerUser);
router.post('/login', loginUser);
router.get('/user', verifyToken, getUser);
module.exports = router;
verifyToken関数の中でTokenから取り出したユーザ情報をgetUser関数で利用します。reqに含まれるuser情報からemailを利用しています。
const getUser = async (req, res) => {
const user = await User.findOne({ email: req.user.email });
if (!user) return res.status(404).json('ユーザは存在しません。');
res.status(200).json(user);
};
REST Clientを利用して動作確認を行います。Tokenはログインの動作確認時に戻されたものを利用します。
###
GET http://localhost:5000/api/users/user
Authorization: Bear eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYxOGExNzExZWEyN2I2ZjEwMGE1ODIzYSIsIm5hbWUiOiJKb2huIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM2NTkzMjY2LCJleHAiOjE2MzY2Nzk2NjZ9.efD0FzwPxKZ4Ze-iyuG8qnlqJQUBLd8b4uyKaTtWBsY
GETリクエストを送信するとユーザ情報が戻されます。
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 235
ETag: W/"eb-UIo/dLunsiB2PzDLmE0kbEvFa6M"
Date: Thu, 11 Nov 2021 02:14:09 GMT
Connection: close
{
"_id": "618a1711ea27b6f100a5823a",
"name": "John",
"email": "john@example.com",
"password": "$2b$10$jevCaXIlAx7oUJGXPXfg3OK1tpiKBHrnk//p/rUVZ32ZZDXyWrX5u",
"createdAt": "2021-11-09T06:37:05.728Z",
"updatedAt": "2021-11-09T06:37:05.728Z",
"__v": 0
}
Tokenの文字を1つ削除すると”アクセストークンが有効ではありません。”と表示されます。Tokenを含めずGETリクエストを送信すると”アクセストークンが含まれていません。”が表示されます。Tokenの検証に失敗するとユーザ情報が取得できないことが確認できました。
ユーザ認証によるアクセス制限
バックエンド側でのTokenの設定が完了したのでフロントエンド側でユーザ認証が完了しているユーザのみダッシュボードにアクセスできるように設定を行います。認証が完了していないユーザがダッシュボードのURLである/(ルート)にアクセスがあるとログイン画面にリダイレクトさせる仕組みが必要となります。
ナビゲーションガードの設定
/(ルート)にアクセスが行われダッシュボードの画面を表示させる前にユーザの認証が完了しているかどうかチェックを行います。認証完了のチェックにVue Routerのナビゲーションガードを利用します。
ルーティングにmetaオプションを設定することができ、requiresAuthをmetaオプションに追加することで認証が必要なルーティングかどうかをわけます。ダッシュボードのルーティングにmetaオプションのrequireAuthを設定します。
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: { requiresAuth: true },
},
//略
requiresAuthがtrueを持つルーティングの場合のみナビゲーションガードを利用してloginにリダイレクトさせます。ナビゲーションガードはrouter¥index.jsファイルに記述します。
//略
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
next('/login');
} else {
next();
}
});
ナビテーションガードを設定後に/(ルート)にアクセスするとログイン画面が表示されることを確認してください。metaオプションのrequiresAuthを設定していないルーティングにアクセスすると/loginにリダイレクトされないことも確認しておいてください。
これだけの設定ではrequiresAuthがtrueのルーティングはすべて/loginにリダイレクトされることになります。そのためrequiresAuthの設定でそのルーティングが認証が必要かどうか分岐させ、その後ログイン認証が完了しているかどうかの分岐を追加する必要があります。
取得したTokenの設定
バックエンドサーバから戻されたTokenをフロントエンド側でどのように設定を行うのか確認を行っていきます。
ログインで取得したTokenを使って/api/users/userからユーザ情報を取得するためにヘッダーのAuthorizationにTokenを設定する必要があります。axiosではデフォルト値に下記のように設定を行うことができます。
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
Tokenをaxiosのヘッダーの初期値に設定後にaxiosでリクエストを送信するとヘッダーにTokenが付与された形でリクエストが送信されます。
login関数の中でログインが成功するとTokenがExpress.jsから戻されるのでTokenをaxiosに設定します。
const login = async () => {
try {
const response = await axios.post('http://localhost:5000/api/users/login', {
email: email.value,
password: password.value,
});
const token = response.data.token;
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
router.push('/');
} catch (err) {
error.value = err.response.data;
}
};
axiosにTokenを設定後、/(ルート)にリダイレクトさせます。現時点ではmetaのrequirsAuthにより/(ルート)にリダイレクトしてもそのまま/loginにリダイレクトされます。
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
next('/login');
} else {
next();
}
});
ログインで認証が確認するために設定したTokenを利用して/api/users/userにアクセスします。ログインが完了して正しいTokenであれば正常に処理が完了するのでそのまま/(ルート)が表示されます。もしTokenに問題がある場合やTokenが設定されていない場合は/loginにリダイレクトするように設定を行います。
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth) {
try {
await axios.get('http://localhost:5000/api/users/user');
next();
} catch (error) {
next('login');
}
} else {
next();
}
});
設定後ログイン画面からログインを行い/(ルート)のダッシュボードのページが表示されることを確認してください。またログイン画面ではなく直接/(ルート)にアクセスした場合はログイン画面にリダイレクトされることを確認してください。
/(ルート)のダッシュボード以外のルーティングにもrequiresAuthを設定することでユーザ認証が完了しているユーザしかアクセスできないようにすることができます。
//略
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: { requiresAuth: true },
},
{
path: '/profile',
name: 'Profile',
component: Profile,
meta: { requiresAuth: true },
},
{
path: '/order',
name: 'Order',
component: Order,
meta: { requiresAuth: true },
},
{
path: '/product',
name: 'Product',
component: Product,
meta: { requiresAuth: true },
},
{
path: '/signup',
name: 'SignUp',
component: SignUp,
},
{
path: '/login',
name: 'Login',
component: Login,
},
];
//略
ログインが完了していない状態で/order, /product,..にアクセスすると/loginにリダイレクトされますがログインが完了しているとページが表示されます。ナビゲーションガードの処理がページの移動毎に実行されるのでブラウザのデベロッパーツールのネットワークタブを見るとバックエンドサーバの/api/users/userにリクエストを送信していることが確認できます。
ログアウト処理
ダッシュボードの作成時にDropdownMenuコンポーネントにログアウトのリンクを設定していたのでリンクではなくclickイベントを追加します。
<li
class="
text-gray-700
dark:text-gray-300
hover:bg-blue-100
dark:hover:bg-gray-700
hover:text-blue-600
dark:hover:text-blue-600
p-2
"
>
<div @click="logout" class="flex items-center space-x-2">
<LogoutIcon class="w-5 h-5" />
<span class="text-sm font-bold">ログアウト</span>
</div>
</li>
logout関数をDropdownMenu.vueファイルに追加します。logout関数の中ではaxiosのヘッダーの初期値に設定したTokenの値をnullにしているだけです。
const logout = () => {
axios.defaults.headers.common['Authorization'] = null;
router.push('/login');
};
ログイン後に右上にあるユーザ画像をクリックして表示されるドロップダウンメニューからログアウトを選択すると/login画面にリダイレクトされます。
Vuexの設定
バックエンドサーバから受け取ったユーザ情報を保存することで現在どのユーザがログインを行なっているかがわかるようになります。保存したユーザ情報に対してどこからでもアクセスできるようにVuexを利用します。
Vuexの初期設定
Vuexを利用するためにはVuexのインストールが必要になります。
% npm install vuex@next --save
Vuexのインストール完了後にsrcフォルダにvuexフォルダ、その下にindex.jsファイルを作成します。index.jsファイルには動作確認のためcountを定義します。
import { createStore } from 'vuex';
export const store = createStore({
state() {
return {
count: 1,
};
},
});
index.jsで作成したstoreをmain.jsファイルでimortしてVueに設定を行います。
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './index.css';
import { store } from './store';
createApp(App).use(router).use(store).mount('#app');
設定したVuexのcountをProfileコンポーネントからアクセスできることを確認します。VuexのstoreにアクセスするためにuseStoreとcomputedを利用します。
<script setup>
import Default from '../layouts/Default.vue';
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const count = computed(() => store.state.count);
</script>
<template>
<Default>プロファイル count:{{ count }}</Default>
</template>
ログイン後に右上にあるユーザ画像のドロップダウンメニューからプロファイルページに移動するとVuexのStoreに保存されているcountが表示されることを確認してください。どのようにVuexに保存されている変数にアクセスできるかがわかりました。
userモジュールの作成
ダッシュボードのアプリケーションが大きくなってくるとstore/index.jsファイルのみに情報を記述していくと管理が煩雑になってくるので用途に応じてモジュール化します。
ユーザ関連の情報のみモジュールとして管理できるようにstoreフォルダの下にmodulesフォルダを作成してその下にuser.jsファイルを作成します。userオブジェクトを作成してnameプロパティにJohnを設定します。
export const user = {
namespaced: true,
state: {
user: {
name: 'John',
},
},
};
store¥index.jsファイルでモジュール化したuserを読み込みます。
import { createStore } from 'vuex';
import { user } from './modules/user';
export const store = createStore({
modules: {
user,
},
});
Profileコンポーネントから設定したuserオブジェクトのnameを表示します。アクセスする際はstore.state.userではなくstore.state.user.userになっていることを確認してください。
<script setup>
import Default from '../layouts/Default.vue';
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const user = computed(() => store.state.user.user);
</script>
<template>
<Default>プロファイル ユーザ情報:{{ user.name }}</Default>
</template>
ログイン後にプロファイルページに移動するとユーザ情報を確認することができます。モジュール化したuser情報にもアクセスすることができました。
user情報の設定
ログイン後にバックエンドサーバの/api/users/userにアクセスを行いユーザ情報を取得します。取得したユーザ情報をstoreに保存できるように設定を行っていきます。
userモジュールにmutationsとactionsを追加します。actionsの中にlogin関数を追加し、ログイン処理が完了した後に/api/users/userにアクセスを行いユーザ情報を取得しています。取得したユーザ情報をmutationsのsetUser関数を利用してstateのuserに設定しています。先ほどまではuserにnameを設定していましたがデフォルト値はnullに設定しています。
import axios from 'axios';
export const user = {
namespaced: true,
state: {
user: null,
},
mutations: {
setUser(state, payload) {
state.user = payload;
},
},
actions: {
login: async ({ commit }, user) => {
try {
let response = await axios.post(
'http://localhost:5000/api/users/login',
{
email: user.email,
password: user.password,
}
);
const token = response.data.token;
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
response = await axios.get('http://localhost:5000/api/users/user');
commit('setUser', response.data);
return response;
} catch (err) {
throw new Error(err.response.data);
}
},
},
};
Login.vueコンポーネントの中でログインの処理を行っていましたがログイン処理はuserモジュールで行うように変更したのでdispatchでuser/loginを指定してログイン処理を実行しています。
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
const store = useStore();
const router = useRouter();
const email = ref('kevin@test.com');
const password = ref('password');
const error = ref('');
const login = () => {
store
.dispatch('user/login', {
email: email.value,
password: password.value,
})
.then(() => {
router.push('/');
})
.catch((err) => {
error.value = err.message;
});
};
ログイン処理が完了するとstoreのユーザ情報が保存されるようになります。ログイン処理を実行してプロファイルページに移動します。
Kevinという名前でユーザ登録を行っているのでプロファイルページにはKevinが表示されます。別のユーザでログインするとログインしたユーザ名が表示されることも確認してください。
ナビゲーションガードの変更
ナビゲーションガードではページに移動する際に/api/users/userにアクセスを行いユーザ認証が行い正しいTokenを持っているかチェックを行っていましたがstoreに保存したuser情報を利用することもできます。user情報を保持できるユーザのみmetaオプションにrequiresAuthがtrueに設定されているルーティングにアクセスすることができます。
import { store } from '../store';
//略
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth) {
if (!store.state.user.user) {
next('login');
} else {
next();
}
} else {
next();
}
});
設定後はログイン認証が完了したユーザのみダッシュボード、プロファイルページに移動することができます。ブラウザの拡張機能のVue.jsのdevtoolなどを利用してVuexのuser情報をnullにしてページ移動すると/loginにリダイレクトされます。
ログアウト処理
現在設定されているログアウト処理はaxiosのヘッダーに設定したtokenをnullにしているだけなのでstoreのuserもnullに設定する必要があります。userモジュールにlogoutの処理を追加します。
mutations: {
setUser(state, payload) {
state.user = payload;
},
clearAuth(state) {
axios.defaults.headers.common['Authorization'] = null;
state.user = null;
},
},
actions: {
login: async ({ commit }, user) => {
//略
},
logout({ commit }) {
commit('clearAuth');
},
},
ログアウト処理が実行されているDropdownMenu.vueファイルでlogout関数を変更します。
import { useStore } from 'vuex';
const store = useStore();
//略
const logout = () => {
store.dispatch('user/logout').then(() => {
router.push('/login');
});
};
設定後、ドロップダウンメニューからログアウトを実行するとstoreからuser情報の削除が行われます。
localStoargeの設定
ブラウザのタブを閉じて再度アクセスを行うとVuexの保存されたユーザ情報は消えているため毎回ログインを行う必要があります。タブ、ブラウザを閉じてもユーザ情報を保持するためにlocalStoargeを利用します。
ログインが完了するとlocalStorageに取得したTokenを保存するためにmutationsのsetTokenを追加しています。
mutations: {
//略
setToken(state, payload) {
localStorage.setItem('accessToken', payload);
},
},
actions: {
login: async ({ commit }, user) => {
try {
let response = await axios.post(
'http://localhost:5000/api/users/login',
{
email: user.email,
password: user.password,
}
);
const token = response.data.token;
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
response = await axios.get('http://localhost:5000/api/users/user');
commit('setUser', response.data);
commit('setToken', token);
return response;
} catch (err) {
throw new Error(err.response.data);
}
},
//略
},
ログイン後にlocalStorageにTokenが保存されているかどうかはデベロッパーツールのアプリケーションのローカルストレージから確認することができます。
ログアウトする際に保存したTokenを削除するためmutationsのclearAuthでTokenの削除を行います。
mutations: {
setUser(state, payload) {
state.user = payload;
},
clearAuth(state) {
axios.defaults.headers.common['Authorization'] = null;
state.user = null;
localStorage.removeItem('accessToken');
},
},
actions: {
login: async ({ commit }, user) => {
//略
},
logout({ commit }) {
commit('clearAuth');
},
},
ログアウト処理を行うとデベロッパーツールのアプリケーションのローカルストレージからTokenの情報は削除されます。
Tokenからユーザ情報の取得
保存したTokenをローカルストレージから取得する処理が必要となります。Tokenの中にはuser情報が含まれているのでTokenをデコードすることでTokenからユーザ情報を取り出しstoreのuserに保存することができます。Tokenをデコードするためにjwt-decodeパッケージのインストールを行います。
% npm install jwt-decode
main.jsファイル内でlocalStorageにTokenが保存されている場合にtokenをjwt-decodeでデコードを行うユーザ情報が取得できるか確認します。
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './index.css';
import { store } from './store';
import axios from 'axios';
import jwt_decode from 'jwt-decode';
const token = localStorage.getItem('accessToken');
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
const user = jwt_decode(token);
console.log(user);
}
createApp(App).use(router).use(store).mount('#app');
一度ログインを行い、ブラウザを閉じて再度アクセスを行ってください。コンソールにユーザ情報が表示されます。localStorageに保存したユーザ情報が取得できることがわかります。
localStorageから取得したユーザ情報をstoreのuserに保存できるようにactionsを追加します。
actions: {
login: async ({ commit }, user) => {
//略
},
logout({ commit }) {
commit('clearAuth');
},
setUser({ commit }, payload) {
commit('setUser', payload);
},
},
main.jsにdispatch(‘user/setUser’)を追加します。
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
const user = jwt_decode(token);
store.dispatch('user/setUser', user);
}
ログイン後にブラウザを閉じてアクセスするとログインすることなくダッシュボード等のページにアクセスすることができます。
ユーザ登録設定
ユーザ登録後もログイン処理が行えるようにSignUp.vueファイルの更新を行います。ユーザ登録が完了したら/(ルート)にリダイレクトしていましたがdispatchのログイン処理を追加し作成に利用したemailとpasswordを利用してログインを行います。
const register = () => {
axios
.post('http://localhost:5000/api/users/register', {
name: name.value,
email: email.value,
password: password.value,
})
.then(() => {
store
.dispatch('user/login', {
email: email.value,
password: password.value,
})
.then(() => {
router.push('/');
})
.catch((err) => {
error.value = err.message;
});
})
.catch((err) => {
error.value = err.response.data.message;
});
};
新たにユーザ登録画面からユーザを登録するとダッシュボードページにリダイレクトされます。
ここまでの設定で簡易的なユーザ認証機能をダッシュボードに実装することができました。