Vue3+Node.jsで認証機能を実装する

# はじめに
サーバー側、RPA開発の経験が主なのですが、フロント側の経験も積みたく、勉強も兼ねてVue.jsを使った社内システムの開発を最近はじめました。
ほぼ初めて触るのでせっかくならば最新バージョンでと思いVue3で実装をしてみましたので、ここに備忘録として作業内容を残します。

> // 余談
> 現時点(2022/04/09)ではナレッジが少ないことと対応ライブラリが少ない事から、今後の開発が辛いことが分かりましたので、社内システムはVue2で作り直そうと思っています。

# 本記事で紹介すること
1. 構成図
1. 完成イメージ
1. VueCLIからVueプロジェクト作成
1. フロントエンド-認証画面の実装
1. Node.jsプロジェクト作成
1. バックエンド-認証処理の実装

※Node.jsとVue.jsのインストールは割愛しております。


# 構成図
ざっくりこんな感じです。
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/a2ae7f5b-6d32-7a16-08c2-635fe79fde38.png)
> // コメント
今回はやっておりませんが、実際に作り上げる時はNode.jsとデータベースを連携させることになると思います。
その際はNode.jsプロジェクトにデータベースのモジュールを入れてあげれば出来るようです。

# 完成イメージ
**ログイン画面**
![ログイン画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/7d2272f7-0ddb-72ca-3794-6c8e225fab07.png)
**認証後に遷移するPage1画面**
![Page1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/6216a3a2-d62e-3a44-15a4-08bbd19c95e0.png)
**Page2画面**
![Page2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/669ae15f-b08c-6c82-2884-86900acd3ea6.png)


# フロントエンド側
## 1. VueCLIからVueプロジェクト作成
参考サイト：https://webrandum.net/vue-cli-gui/

**プロジェクトの生成**
まず、ターミナルで開発するフォルダに移動し、VueCLI起動コマンドを実行します。
※プロジェクト作成時のデフォルトがVueCLIを起動したカレントパスになるため。
※作成時に作成先を指定することもできます。
```terminal
$ cd ｛開発フォルダパス｝
$ vue ui
```
起動メッセージが表示された後、自動的にブラウザが起動しVueのGUI画面が表示されます。

次にVueプロジェクトマネージャ画面から`作成`＞`ここに新しいプロジェクトを作成する`をクリックしてください。
![プロジェクト作成1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/df1b4c52-00c8-8c54-cde3-06b7da54c392.png)

**プロジェクトの設定**
プロジェクト設定画面が表示されるため、順番に設定していきます。
今回は以下の様に設定しました。

| 項目名 | 入力値 |
|:-:|:-|
| プロジェクトフォルダ | {フロント側プロジェクトと分かるような名前} |
| パッケージマネージャ | デフォルト |
| 追加オプション | 存在する場合、対象フォルダを上書きする：OFF <br/> 初心者向けインストラクションなしでプロジェクトを構築：OFF |
| Git初期化 | OFF |
| プリセット | デフォルトプリセットVue3 |

プロジェクトの設定ができたら`プロジェクトを作成する`をクリックしてプロジェクトを生成します。
完了するとプロジェクトのダッシュボード画面に移動します。

**プロジェクトの確認**
作成したVueプロジェクトのダッシュボード画面から`タスク`メニューをクリックし、`タスクの実行`ボタンをクリック。
起動処理完了後、`アプリを開く`ボタンをクリックしてサンプル画面が表示されることを確認します。
![プロジェクト確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517685/18594cf1-b579-1e92-014d-9a54b4c292f7.png)

> // コメント
起動に失敗した場合、`出力`ボタンをクリックするとエラー内容を確認することができます。

> // コメント
プロジェクト作成以降はVueCLIを毎回起動せずにコマンドで起動した方が楽だと思います。
その際は以下の通りコマンドになります。
`cd ｛作成したフロントエンド側プロジェクトのパス｝`
`npm run serve`
起動後、`http://localhost:8080`にアクセス。

**プラグインの設定**
`プラグイン`メニューに移動し、プラグインをインストールします。
今回は以下を追加しました。

| プラグイン | 機能 |
|:-:|:-|
| VueRouter | ルーティング制御。画面遷移を制御できる。 |
| Vuex | すべてのコンポーネントでデータを一元管理するための仕組み |
| ESlint | JavaScript のための静的検証ツール |

**依存関係の設定**
`依存`メニューに移動し、依存をインストールします。

| 依存 | 機能 |
|:-:|:-|
| axios | HTTPクライアント。REST-APIの実装を容易に行えるようになる。 |
| Bootstrap | CSSフレームワーク。Vueに最適化されたBootstrapVueもありますがまだVue3に未対応のため今回はこちらを使用。 |

## 2. 認証画面の実装
参考サイト1：https://qiita.com/tky_st/items/03faba81129e4877c3ea
参考サイト2：https://www.chuken-engineer.com/entry/2020/08/21/163721
参考サイト3：https://v3.ja.vuejs.org/guide/migration/global-api.html#新しいグローバル-api-createapp

以下、ファイルを追加・編集していきます。
| ファイル | 説明 |
|:-:|:-|
|src/main.js|アプリの定義|
|src/store/index.js|アプリ全体の状態を管理するための定義|
|src/router/index.js|画面遷移を制御|
|src/components/Login.vue|認証画面の画面定義|
|src/components/Page1.vue|認証成功時に最初に遷移する画面の定義|
|src/components/Page2.vue|認証成功時のPage1から遷移出来る画面の定義|

**src/main.js**
まず、vueの定義をします。
Vue2系とVue3系では記述の仕方が変更されたようで、axiosの定義方法を理解するのに少し手こずりました。
```javascript:src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import axios from 'axios'
import store from './store'
import router from './router'
import 'bootstrap/dist/css/bootstrap.min.css'

const app = createApp(App)

// axiosを使用できるように定義
app.config.globalProperties.$axios = axios.create({
  baseURL: 'http://localhost:3000/'
})

app.use(router)
app.use(store)
app.mount('#app')
```
**src/store/index.js**
認証状態を管理するための定義をします。
```javascript:src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    isLogin: false,
    userId: ''
  },
  mutations: {
    auth(state, user){
      state.isLogin = true;
      state.userId = user;
    }
  },
  actions: {
    fetch(context, user){
      context.commit('auth', user);
    }
  },
  modules: {
  }
})
```

**src/router/index.js**
VueRouterを使って画面遷移の定義をします。
```javascript:src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Page_1 from '../components/Page1.vue'
import Page_2 from '../components/Page2.vue'
import Login from '../components/Login.vue'
import Store from '../store/index.js'

const routes = [
  {
    path: '/page1',
    name: 'page1',
    component: Page_1,
    meta: {
      requiresAuth: true   // 認証済の時のみ閲覧可能となるように定義
    }
  },
  {
    path: '/page2',
    name: 'page2',
    component: Page_2,
    meta: {
      requiresAuth: true   // 認証済の時のみ閲覧可能となるように定義
    }
  },
  {
    path: '/',
    name: 'home',
    component: Login
  },
  {
    path: '/Login',
    name: 'login',
    component: Login
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  base: process.env.BASE_URL,
  routes
})

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!Store.state.isLogin) {
      // 認証されていない時、認証画面へ遷移
      next({
        path: '/Login',
        query: {
          redirect: to.fullPath
        }
      })
    } else {
      next();
    }
  } else {
    next(); 
  }
});

export default router
```

**src/components/Login.vue**
`src/components`フォルダの下にログイン画面を作ります。
```vue:src/components/Login.vue
<template>
  <div class="login">
    <h2>Sign in</h2>
    <div class="container" style="width:500px">
      <div class="row align-items-center">
        <div class="col-md-4">アカウント名</div>
        <div class="col-md-3">
          <input v-model="authId" type="text" placeholder="Username">
        </div>
      </div>
      <div class="row align-items-center">
        <div class="col-md-4">パスワード</div>
        <div class="col-md-3">
          <input v-model="authPass" type="password" placeholder="Password">
        </div>
      </div>
      <div class="row align-items-center">
        <div class="col-md-12">
          {{ msg }}
        </div>
      </div>
      <div class="row align-items-center">
        <div class="col-md-12">
          <button class="btn btn-info btn-block login" v-on:click="post">ログイン</button>
        </div>
      </div>
    </div>
  </div>
</template>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.login {
  margin-top: 20px;

  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center;
}
input {
  margin: 10px 0;
  padding: 10px;
}
</style>
<script>
export default {
  name: 'Login',
  data () {
    return {
      showPassword : false,
      msg : 'userIDとpasswordを入力して下さい',
      authId : '',
      authPass : ''
    }
  },
  methods: {
    async post() {
      const data = { id : this.authId, pass : this.authPass };
      this.msg = await this.$axios.post('/fnclogin', data)    // バックエンド側にPOST
      .then(function (response) {
        console.log(response);
        return response.data.message;
      })
      .catch(function (error) {
        console.log(error);
        return error.message;
      });
      
      if(this.msg == 'OK'){
        this.$store.dispatch("fetch", this.authId);
        this.$router.push('/Page1');
      }
    }
  }
};
</script>
```

**src/components/Page1.vue**
`src/components`フォルダの下に認証後に遷移するページを作ります。
```vue:src/components/Page1.vue
<template>
  <div>
      <h1>ここはページ1です</h1>
      <h3>user:{{$store.state.userId}}</h3>
      <router-link to="/page2">ページ2へ</router-link>
  </div>
</template>
```

**src/components/Page2.vue**
`src/components`フォルダの下にPage1画面から遷移するページを作ります。
```vue:src/components/Page2.vue
<template>
  <div>
      <h1>ここはページ2です</h1>
      <h3>user:{{$store.state.userId}}</h3>
      <router-link to="/page1">ページ1へ</router-link>
  </div>
</template>
```

# バックエンド側
## 1. Node.jsプロジェクト作成
**ExpressGeneratorをインストール**
Node.jsのWEBフレームワークをインストールします。
```console
$ npm install -g express-generator
```

**プロジェクトの作成**
ExpressGeneratorを使ってバックエンド側のプロジェクトを作成します。
```console
$ cd ｛開発フォルダパス｝
$ express -e ｛バックエンド側プロジェクトと分かるような名前｝
```

**Node.jsパッケージをインストール**
作成したプロジェクトにNode.jsのパッケージをインストールします。
```console
$ cd ｛作成したバックエンド側プロジェクトのパス｝
$ npm install
```

**起動確認**
バックエンドプロジェクトを起動。
`http://localhost:3000`にアクセスしてサンプル画面が表示されるか確認します。
```console
$ npm start
```

**corsのインストール**
フロントエンドとバックエンドでHTTP通信出来るようにするため、バックエンドプロジェクトに`cors`をインストールします。
```console
$ cd ｛バックエンド側プロジェクトのパス｝
$ npm install cors
```

## 2. 認証処理の実装
以下、ファイルを追加・編集していきます。
| ファイル | 説明 |
|:-:|:-|
|app.js|アプリの定義|
|routes/login.js|認証機能の実装|

**app.js**
アプリ定義に認証機能のルートを定義します。
```javascript:app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const cors = require('cors')

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var loginRouter = require('./routes/login');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// cors設定
app.use(cors());

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  next();
 });

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/fnclogin', loginRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
```

**routes/login.js**
`routes`フォルダの下に認証処理を作ります。
```javascript:routes/login.js
const express = require('express');
const router = express.Router();

router.post('/', function(req, res, next) {
  if(req.body.id == 'test' && req.body.pass == 'test'){
    res.send({
      message: 'OK'
    })
  }else{
    res.send({
      message: '認証エラー'
    })
  }
 
})

module.exports = router;
```

# 動作確認
フロントエンド側、バックエンド側をそれぞれ起動し動作を確認します。

**フロントエンド側の起動**
ターミナルを起動して以下コマンドを実行
```console
$ cd ｛フロントエンド側プロジェクトのパス｝
$ npm run serve
```

**バックエンド側の起動**
フロントエンドのターミナルとは別に新たにターミナルを起動して以下コマンドを実行
```console
$ cd ｛バックエンド側プロジェクトのパス｝
$ npm start
```

**画面にアクセス**
`http://localhost:8080/`にアクセス。
`src/components/Login.vue`で作ったログイン画面が表示されるので、以下アカウント情報を入力してログインします。
正しく動作すれば`src/components/Page1.vue`で作ったPage1画面に遷移するはずです。
* アカウント名:test
* パスワード:test

# 所感
分からない事だらけの状態で記事を参考に作り始めたため、色々と手こずりました。
参考にさせてもらった記事がバージョン違いなどを理由に実装方法が異なっていたため、それの意図やバージョンごとの実装方法の違いを知るところからのスタートでした。
100%理解はできていませんが、大変勉強になりました。
最初に書いたとおり、社内システムの方はVue2で進めようと思うのでまたアウトプットできることがあれば記事にしたいと思います。