■ 主の PC環境一覧
OS:Windows11(WSL2:Ubuntu 22.04.4 LTS)
nvm:v18.16.0
npm:v9.5.1
MySQL:8.4.0
フロントエンド側(React)の最終的なファイル構成一覧
./login-form
└── form
├── public
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── assets
│ │ └── styles
│ │ └── Login.css
│ ├── index.css
│ ├── index.tsx
│ ├── pages
│ │ └── LoginForm.tsx
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
├── .gitignore
├── .env
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
バックエンド側(Node.js)の最終的なファイル構成一覧
./login-form
└── server
├── app.ts
├── package-lock.json
├── package.json
├── src
│ ├── config
│ │ └── db.ts
│ ├── controllers
│ │ └── authLoginController.ts
│ ├── models
│ │ └── authLoginModel.ts
│ └── routes
│ └── authLoginRoutes.ts
└── tsconfig.json
前回の記事で環境構築手順を説明していますので、まだ読まれていない方は是非。
■ 初めに
今回の完成である実装を一式 GitHubにて公開していますので、よければどうぞ。
はい、頑張っていきましょう。
[1]フロントエンド側(React)実装
フロントエンド側で使用する(新規作成する)ファイルの作成から行います。
以下に示す「create-dev-files.sh」ファイルを「form」ディレクトリ内に作成して実行してください。
※ なお、環境構築編で設計パターンに伴う各ディレクトリを作成していることを前提とした構成です。
#!/bin/bash
files=(
".env"
"src/assets/Login.css"
"src/pages/LoginForm.tsx"
)
for file in "${files[@]}"; do
touch "$file"
done
$ chmod +x create-dev-files.sh
$ ./create-dev-files.sh
[1.1]エントリーポイントを修正(index.tsx)
index.tsx
ファイルは、React アプリケーションのエントリーポイントとして機能します。
本コードスニペットは、アプリケーションが起動するとき最初に読み込まれ、通常はアプリケーションのルートコンポーネントをレンダリングします。
import React from 'react';
import ReactDOM from 'react-dom/client';
import reportWebVitals from './reportWebVitals';
import LoginForm from './pages/LoginForm';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<LoginForm />
</React.StrictMode>
);
reportWebVitals();
コードスニペットの詳細
- モジュールのインポート
- React
- React自体をインポートしています。
- ReactDOM
- React DOMライブラリをインポートしています。
本ライブラリは、Reactコンポーネントを DOMにマッピングするためのツールを提供します。
- React DOMライブラリをインポートしています。
- reportWebVitals
- パフォーマンス測定に関連する関数をインポートしています。
本関数は、Webページのパフォーマンス指標を収集し、分析するために使用されます。
- パフォーマンス測定に関連する関数をインポートしています。
- LoginForm(実装はこちら)
- ログインフォームを表示するためのコンポーネントをインポートしています。
- React
- React DOMの初期化
ReactDOM.createRoot
関数を使用して、document.getElementById('root')
で取得した HTML要素に対する Reactの根ノードを生成しています。
この方法は、コンポーネントのライフサイクルをより柔軟に制御できる新しい APIです。
- コンポーネントのレンダリング
root.render
関数を使用して、LoginFormコンポーネントをレンダリングしています。
本コンポーネントは、ユーザーがログイン情報を入力してログインできるようにするものです。
- パフォーマンス測定
reportWebVitals
関数の呼び出しは、Webページのパフォーマンスを監視し、改善の余地がある箇所を特定するためのツールとして利用されます。
これにより、アプリケーションのパフォーマンスを継続的に向上させることが可能になります。
[1.2]環境変数を定義
.env
は環境変数を定義するために使用します。
これは、開発環境や本番環境で異なる設定値を簡単に切り替えることができるようにするための仕組みです。
また、「.env」ではなくても「開発環境:
」や「本番環境:.env
.development
」のようにサフィックスを付与したファイル名でも問題ありません。.env
.production
# crypto-js
REACT_APP_ENCRYPTION_USER = xxxxxxxxxx
REACT_APP_ENCRYPTION_PASS = yyyyyyyyyy
[1.3]ログイン認証画面を実装(LoginForm.tsx)
以下のコードスニペットは、ユーザーが自身のIDとパスワードを入力してログインできるようにすることです。
具体的には、ユーザーがフォームに情報を入力すると、その情報がリアルタイムで状態に反映され、ログインボタンを押すと、入力された情報がサーバーに送られ、サーバーからのレスポンスに基づいてログイン処理が行われます。
また、本コンポーネントではユーザー情報の暗号化を行っており、これはセキュリティ上の配慮です。暗号化により、通信中の情報が第3者によって盗聴されるリスクを軽減することができます。
import React, { useState, ChangeEvent, FormEvent } from 'react';
import axios from 'axios';
import CryptoJS from 'crypto-js';
import { Container, Row, Col, Form, Button } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../assets/styles/Login.css';
/**
* @brief ログイン認証画面
* @returns コンポーネント
*/
const LoginForm: React.FC = () => {
const [userInfo, setUserInfo] = useState<{username: string; password: string;}>( {
username: '',
password: ''
});
/**
* @brief 'username'及び 'passwprd'変更ハンドラ
* @param {*} e イベント
*/
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setUserInfo( {
...userInfo,
[name]: value
});
};
/**
* @brief ログインボタン押下ハンドラ
* @param {*} e イベント
*/
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
/**
* --------------------------------------------------------------------------------------------------------------
* Encryption of user information
* --------------------------------------------------------------------------------------------------------------
*/
const encryptedUserInfo: {
username: string | undefined;
password: string | undefined;
} = {
username: undefined,
password: undefined
}
const encryptionUserKey: string | undefined = process.env.REACT_APP_ENCRYPTION_USER;
const encryptionPassKey: string | undefined = process.env.REACT_APP_ENCRYPTION_PASS;
encryptedUserInfo.username = CryptoJS.AES.encrypt(userInfo.username, (encryptionUserKey as string)).toString();
encryptedUserInfo.password = CryptoJS.AES.encrypt(userInfo.password, (encryptionPassKey as string)).toString();
/**
* --------------------------------------------------------------------------------------------------------------
*/
axios.post('http://localhost:5000/api/auth/login', encryptedUserInfo, {
headers: {
'Content-Type': 'application/json'
}
})
.then((res) => {
if (res.data.status === 200) {
// OK
}
else {
// NG
}
console.log(`${res.status}: ${res.headers}`)
})
.catch((err) => {
console.error(`[Error] ${err}`);
});
};
return (
<Container fluid className="d-flex vh-100">
<Row className="m-auto align-self-center w-100">
<Col xs={12} md={6} lg={4} className="mx-auto">
<div className="p-4 shadow rounded">
<h3 className="text-center mb-4">
ログイン認証画面
</h3>
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formBasicUsername" className="mb-3 mt-5">
<Form.Control
type="text"
name="username"
placeholder="username"
value={userInfo.username}
onChange={handleChange}
required
autoComplete="username"
/>
</Form.Group>
<Form.Group controlId="formBasicPassword" className="mb-3 mt-4">
<Form.Control
type="password"
name="password"
placeholder="password"
value={userInfo.password}
onChange={handleChange}
required
autoComplete="current-password"
/>
</Form.Group>
<Button variant="skeleton" type="submit" className="btn btn-outline-info w-100 mt-4">
Login
</Button>
</Form>
</div>
</Col>
</Row>
</Container>
);
};
export default LoginForm;
コードスニペットの詳細
- コンポーネントの宣言と状態の初期化
useState
フックを使用して、userInfo
という名前のオブジェクトを保持する状態を初期化しています。
本オブジェクトには、username
とpassword
の 2つのプロパティがあり、それぞれの値は空文字列で初期化しています。
- 入力フィールドの変更ハンドラ
handleChange
関数は、ユーザーがログインフォームのusername
またはpassword
フィールドに入力するたびに呼び出されます。
本関数は、新しい入力値を受け取り、userInfo
状態を更新します。
- ログインフォームの送信
handleSubmit
関数は、ログインフォームが送信されたときに呼び出されます。
本関数内で、まずユーザー情報を暗号化し、次に暗号化された情報をサーバーに POSTリクエストとして送信します。
暗号化にはCryptoJS.AES.encrypt
関数を使用しています。
- サーバーからのレスポンスの処理
- サーバーからのレスポンスを受け取った後、ステータスコードが 200であるかどうかをチェックし、成功した場合には何もしない(コメントアウトされている部分)、失敗した場合にはエラーメッセージを表示します。
- UIコンポーネントのレンダリング
- コンポーネントの
return
文では、ログインフォームの UIをレンダリングしています。
ここでは、react-bootstrap
ライブラリを使用してフォームのレイアウトを整え、onChange
イベントハンドラを各入力フィールドに割り当てています。
- コンポーネントの
[1.4]ログイン認証画面のスタイルシート作成
::placeholder {
text-align: center;
}
コードスニペットの詳細
- セレクタ
::placeholder
は、CSSの疑似要素の一種で、特定の条件下でスタイルを適用することができます。
この場合、::placeholder
はプレースホルダーテキストに対してスタイルを適用します。
- プロパティ
text-align: center;
は、プレースホルダーテキストの位置を中央揃えにするための CSSプロパティです。
[2]バックエンド側(Node.js)実装
バックエンド側で使用する(新規作成する)ファイルの作成から行います。
以下に示す「create-dev-files.sh」ファイルを「server」ディレクトリ内に作成して実行してください。
※ なお、環境構築編で設計パターンに伴う各ディレクトリを作成していることを前提とした構成です。
#!/bin/bash
files=(
".env"
"config/db.ts"
"controllers/authLoginController.ts"
"models/authLoginModels.ts"
"routes/authLoginRoutes.ts"
)
for file in "${files[@]}"; do
touch "$file"
done
$ chmod +x create-dev-files.sh
$ ./create-dev-files.sh
[2.1]エンドポイントを実装(app.ts)
以下のコードスニペットは、Expressを用いて Web APIを提供するサーバーの基本的な構造を定義しています。
特に、認証関連のエンドポイントを設定し、CORSポリシーを適応することでフロントエンド側との通信を可能にします。
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { router as authRoutes } from './src/routes/authLoginRoutes';
dotenv.config();
const app: express.Express = express();
app.use(express.json());
app.use(cors());
// POST: ログイン認証
app.use('/api/auth', authRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
コードスニペットの詳細
- モジュールのインポート
- express
- Expressをインポートしてアプリケーションを作成します。
- cors
- CORS(Cross-Origin Resource Sharing)ポリシーを適用するために使用されます。
これにより、異なるオリジンのリクエストが許可されます。
- CORS(Cross-Origin Resource Sharing)ポリシーを適用するために使用されます。
- dotenv
- 環境変数をロードするためのライブラリです。
これにより、アプリケーションの設定情報を.env
ファイルから読み込むことができます。
- 環境変数をロードするためのライブラリです。
- authRoutes
- 認証関連のルーティングを含むモジュールをインポートしています。
このモジュールは、認証処理に関する HTTPメソッド(GET、POSTなど)に対応したエンドポイントを定義しています。
- 認証関連のルーティングを含むモジュールをインポートしています。
- express
- 環境変数の読み込み
dotenv.config();
.env
ファイルから環境変数を読み込みます。
これにより、秘密鍵やデータベース接続情報などの機密情報を安全に管理できます。
- Expressアプリケーションの初期化
const app: express.Express = express();
- Expressアプリケーションを初期化します。
- ミドルウェアの設定
app.use(express.json());
- リクエストボディを JSON形式として解析するミドルウェアを追加します。
app.use(cors());
- CORSポリシーを有効にするミドルウェアを追加します。
- ルーティングの設定
/api/auth
への POSTリクエストに対して、authRoutes
を使用してルーティングを設定しています。
これにより、ユーザーのログイン認証処理が行われます。
- サーバーの起動
app.listen(PORT, ...)
で指定されたポートでサーバーを起動します。
デフォルトではポート5000を使用しますが、環境変数process.env.PORT
が設定されている場合はその値を優先します。
[2.2]環境変数を定義
.env
は環境変数を定義するために使用します。
# Database
DB_HOST = localhost
DB_USER = user
DB_PASSWORD = pass
DB_NAME = workspace
# crypto-js
ENCRYPTION_USER = xxxxxxxxxx
ENCRYPTION_PASS = yyyyyyyyyy
[2.3]APIエンドポイントの設定処理を実装(authLoginRoutes.ts)
以下のコードスニペットは、Expressを使用して APIエンドポイントを設定しています。
具体的には、ログイン認証に関する POSTリクエストを処理するエンドポイントを定義しています。
import express from 'express';
import { authLogin } from '../controllers/authLoginController';
export const router = express.Router();
// POST: ログイン認証
router.post('/login', authLogin);
コードスニペットの詳細
- モジュールのインポート
- express
- Expressをインポートしてアプリケーションを作成します。
- authLogin
- ログイン認証処理を担当するコントローラーをインポートしています。
- express
- ルーターインスタンスの作成
express.Router()
を使用して、ルーターインスタンスを作成しています。
このルーターインスタンスは、複数のルート(URLパス)をグループ化し、共通のミドルウェアを適用するのに便利です。
- POSTリクエストのルーティング
router.post('/login', authLogin);
で、/login
へのPOSTリクエストを受け付けるルートを定義しています。
本リクエストは、authLogin
コントローラーによって処理されます。
[2.4]ログイン認証処理を実装(authLoginController.ts)
本コードスニペットは、Expressを使用したログイン認証処理を実装するためのコントローラー関数です。
具体的には、クライアントから受け取ったユーザー情報を複合化し、その情報を使用してデータベースからユーザー情報を検索し、認証に成功した場合に成功メッセージを返す、といった処理を行っています。
import { Request, Response } from 'express';
import { RowDataPacket } from 'mysql2';
import CryptoJS from 'crypto-js';
import { getUserInfo } from '../models/authLoginModel'
/**
* @brief ログイン認証
* @param req リクエスト
* @param res レスポンス
* @returns ログイン認証結果
*/
export async function authLogin (req: Request, res: Response)
{
let userInfo:
{
username: string,
password: string
};
userInfo = {...req.body};
/**
* ------------------------------------------------------------------------------------------------------------------------
* Decryption of user information
*/
const bytesUser: CryptoJS.lib.WordArray = CryptoJS.AES.decrypt(userInfo.username, (process.env.ENCRYPTION_USER as string));
const bytesPass: CryptoJS.lib.WordArray = CryptoJS.AES.decrypt(userInfo.password, (process.env.ENCRYPTION_PASS as string));
const decryptedUser: string = bytesUser.toString(CryptoJS.enc.Utf8);
const decryptedPass: string = bytesPass.toString(CryptoJS.enc.Utf8);
/**
* ------------------------------------------------------------------------------------------------------------------------
*/
try
{
const [results, fields] = await getUserInfo(decryptedUser, decryptedPass);
let rows: RowDataPacket[] = (results as RowDataPacket[]);
if (rows.length === 0)
{
res.status(401).json('ユーザー認証に失敗しました。');
return;
}
res.status(200).json('ユーザー認証が成功しました。');
rows.forEach(result => {
console.log(`ユーザーID: ${result.id}, ユーザー名: ${result.username}, パスワード: ${result.password}`);
});
}
catch (err)
{
res.status(500).json(`データベースからユーザー情報の取得に失敗しました。(${err})`);
}
return res;
};
コードスニペットの詳細
- モジュールのインポート
- Request, Response
- Expressからリクエストとレスポンスオブジェクトをインポートしています。
- RowDataPacket
- MySQL2パッケージから、MySQLの結果セットを扱うための型をインポートしています。
- CryptoJS
- 暗号化ライブラリをインポートしています。
本ライブラリを使用して、ユーザー情報を複合化します。
- 暗号化ライブラリをインポートしています。
- getUserInfo
- ユーザー情報をデータベースから取得するためのモデル関数をインポートしています。
- Request, Response
- リクエストボディの受け取りとデコード
- クライアントから送信されたリクエストボディ(ユーザー名とパスワード)を取得し、それらを複合化して元の情報に戻します。
複合化にはCryptoJS.AES.decrypt
関数を使用しています。
- クライアントから送信されたリクエストボディ(ユーザー名とパスワード)を取得し、それらを複合化して元の情報に戻します。
- データベースからのユーザー情報の取得
- 複合化されたユーザー情報を
getUserInfo
関数に渡して、データベースから該当するユーザー情報を検索します。
- 複合化されたユーザー情報を
- 認証結果の処理
- 検索結果がない場合(認証失敗)は、
401
Unauthorizedのステータスコードとエラーメッセージをレスポンスとして返します。 - 検索結果がある場合(認証成功)は、
200
OKのステータスコードと成功メッセージをレスポンスとして返します。
- 検索結果がない場合(認証失敗)は、
[2.5]データベース操作処理を実装(authLoginModel.ts)
以下のコードスニペットは、Node.jsアプリケーションにおけるデータベース操作を実現するための関数です。
具体的には、指定されたユーザー名とパスワードに一致するユーザー情報をデータベースから検索するためのものです。
import { pool } from '../config/db';
import { QueryResult, FieldPacket } from 'mysql2';
/**
* @brief ユーザー情報取得
* @param username ユーザー名
* @param password パスワード
* @returns クエリ実行結果
*/
export async function getUserInfo (username: string, password: string,): Promise<[QueryResult, FieldPacket[]]>
{
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
return pool.query(query, [username, password]);
};
コードスニペットの詳細
- モジュールのインポート
- pool
- データベース接続プールを管理するためのオブジェクトをインポートしています。
このpool
は../config/db
からエクスポートされているものを使用しています。
- データベース接続プールを管理するためのオブジェクトをインポートしています。
- QueryResult, FieldPacket
- MySQL2パッケージから、クエリ実行結果とフィールドパケットの型をインポートしています。
- pool
- 関数の定義
getUserInfo
関数は、ユーザー名とパスワードを引数に取り、Promiseを返す非同期関数です。
本関数は、データベースからユーザー情報を検索するクエリを実行し、その結果を返します。
- SQLクエリの構築
- 複合化されたユーザー情報を
getUserInfo
関数に渡して、データベースから該当するユーザー情報を検索します。
- 複合化されたユーザー情報を
- 認証結果の処理
- SQLクエリ文字列を定義し、
?
プレースホルダーを使用してユーザー名とパスワードを挿入します。
これにより、SQLインジェクション攻撃を防ぐことができます。
- SQLクエリ文字列を定義し、
- クエリの実行と結果の返却
pool.query
関数を使用して、構築したクエリを実行し、その結果を返します。
本関数は非同期的に動作し、Promiseを返します。
■ 実行
□ データベース側(MySQL)
# 今回、開発環境編で作成したコンテナIDを検索
$ docker ps -a | grep ws_mysql
---
fasdoifuasdf mysql:latest "docker-entrypoint.s…" 12 days ago Exited (0) 47 seconds ago ws_mysql
# 該当のコンテナIDを指定して起動
$ docker start fasdoifuasdf
□ フロントエンド側(React)
# フロントエンド側(React)プロジェクトへ移動
$ cd login-form/form
# nodeを有効化して実行
$ nvm use
$ npm start
---
Compiled successfully!
You can now view form in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.1.1:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
webpack compiled successfully
Files successfully emitted, waiting for typecheck results...
Issues checking in progress...
No issues found.
□ バックエンド側(Node.js)
# バックエンド側(Node.js)プロジェクトへ移動
$ cd login-form/server
# nodeを有効化して実行
$ nvm use
$ npm start
---
> server@1.0.0 start
> npx ts-node app.ts
Server running on port 5000
データベースに接続に成功しました
■ 最後に
では、起動したログインフォームにユーザー情報を入力してログインボタンを押下してみてください。
ここまで本当にお疲れ様でした。
私も大変疲れました。
学んだことは、記事にしている時に改めて記憶に定着しているなぁと実感します。
良い、Webアプリケーションライフを!!
comment 📝