📝

Nginx + PHP + MariaDB でログイン機能を作る

10 min read

前回の記事 で作成した掲示板にログイン機能を搭載してみます。

下準備

データベースにテーブルを追加します。

$ sudo mysql
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 157
Server version: 10.3.29-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> USE bbs;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [bbs]> SHOW TABLES;  # 前回作成分
+---------------+
| Tables_in_bbs |
+---------------+
| kakiko        |
+---------------+
2 rows in set (0.000 sec)

MariaDB [bbs]> CREATE TABLE user (  # ユーザ管理用テーブルの作成
    -> name varchar(64) NOT NULL PRIMARY KEY,
    -> hash varchar(256) NOT NULL,
    -> sid varchar(256)
    -> );
Query OK, 0 rows affected (0.024 sec)

MariaDB [bbs]> DESC user;  # 確認
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(64)  | NO   | PRI | NULL    |       |
| hash  | varchar(256) | NO   |     | NULL    |       |
| sid   | varchar(256) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
3 rows in set (0.001 sec)

MariaDB [bbs]> # ユーザに権限を付与
MariaDB [bbs]> GRANT SELECT,INSERT,UPDATE ON user TO bbsuser@localhost IDENTIFIED BY 'bbspasswd';
Query OK, 0 rows affected (0.001 sec)

MariaDB [bbs]> QUIT
Bye

スクリプトの作成、編集

スクリプトをそれぞれ編集します。前回は掲示板の書き込み処理を別スクリプトに分割していましたが、今回は一つにまとめています。また、投稿者名はログイン時に入力されたものを利用します。

$ sudo vim /var/www/html/bbs/index.php
$ sudo vim /var/www/html/bbs/signin.php
$ sudo vim /var/www/html/bbs/signup.php
$ sudo vim /var/www/html/bbs/signout.php
/var/www/html/bbs/index.php
<?php
const dsn = 'mysql:host=localhost;dbname=bbs';
const dbuser = 'bbsuser';
const dbpass = 'bbspasswd';
const recvsql = 'INSERT INTO kakiko (name,msg,date) VALUES (:name,:msg,:date)';
const dispsql = 'SELECT id,name,msg,date FROM kakiko';
const namesql = 'SELECT name FROM user WHERE sid=?';
const sname = 'BBSLOGIN';

session_name(sname);
session_start();

# ログインしていなければ ./signin.php へ
if (empty($_SESSION['sid'])) {
  header('HTTP/1.1 303 See Other');
  header('Location: ./signin.php');
  exit();
}

# ユーザ名の取得
try {
  $pdo = new PDO(dsn, dbuser, dbpass);
  $stmt = $pdo->prepare(namesql);
  $stmt->execute([$_SESSION['sid']]);
} catch (PDOException $e) {
  $error = $e->getMessage();
  die("Something Wrong ($error)");
}
$user = $stmt->fetchColumn();

# POST (= 投稿を受信) だったら書き込み処理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (empty($_POST['msg']))
    goto endp;

  # 投稿を書き込み
  try {
    $pdo = new PDO(dsn, dbuser, dbpass);
    $stmt = $pdo->prepare(recvsql);
    $stmt->bindValue(':name', $user, PDO::PARAM_STR);
    $stmt->bindValue(':msg', $_POST['msg'], PDO::PARAM_STR);
    $stmt->bindValue(':date', date('Y-m-d H:i:s'), PDO::PARAM_STR);
    $stmt->execute();
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }

endp:
  header('HTTP/1.1 303 See Other');
  header('Location: ./');
  exit();
}

# ユーザ名をエスケープする
$user = htmlspecialchars($user);

# 投稿一覧を取得
try {
  $stmt = $pdo->prepare(dispsql);
  $stmt->execute();
} catch (PDOException $e) {
  $error = $e->getMessage();
  die("Something Wrong ($error)");
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>簡易掲示板</title>
</head>
<body>
<h1>簡易掲示板</h1>
<div>
<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { ?>
<div class="kakiko">
<div class="kakiko_header">
<span class="id"><?= $row['id'] ?></span>
<span class="name"><?= htmlspecialchars($row['name']) ?></span>
</div>
<div>
<pre class="msg"><?= htmlspecialchars($row['msg']) ?></pre>
</div>
<div class="date">
<?= $row['date'] ?>
</div>
</div>
<hr>
<?php } /* while */ ?>
</div>
<form action="./" method="POST">
<fieldset>
<legend>新規投稿(https://)</legend>
<div>
<label for="msg">投稿内容:</label>
<div>
<textarea name="msg" id="msg" required></textarea>
</div>
</div>
<hr>
<input type="submit">
<input type="reset">
</fieldset>
</form>
<p><a href="./signout.php">サインアウト</a></p>
</body>
</html>
/var/www/html/bbs/signin.php
<?php
const dsn = 'mysql:host=localhost;dbname=bbs';
const dbuser = 'bbsuser';
const dbpass = 'bbspasswd';
const insql = 'UPDATE user SET sid=? WHERE name=?';
const cksql = 'SELECT COUNT(*) FROM user WHERE name=?';
const hasql = 'SELECT hash FROM user WHERE name=?';
const sname = 'BBSLOGIN';

# POST (= ログイン情報を受信) だったら
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (empty($_POST['name']) || empty($_POST['pass']))
    goto endp;

  # ユーザ名に合致するものがあるか?
  try {
    $pdo = new PDO(dsn, dbuser, dbpass);
    $stmt = $pdo->prepare(cksql);
    $stmt->execute([$_POST['name']]);
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }
  if ($stmt->fetchColumn() === '0') {
    $name = $_POST['name'];
    die("Unknown name. ($name)");
  }

  # パスワードは合致するか?
  try {
    $stmt = $pdo->prepare(hasql);
    $stmt->execute([$_POST['name']]);
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }
  $hash = $stmt->fetchColumn();

  if (!password_verify($_POST['pass'], $hash)) {
    die("Wrong Password");
  }

  # セッション開始
  session_name(sname);
  session_start();
  session_regenerate_id(true);
  $_SESSION['sid'] = exec('uuidgen');

  # セッション ID を書き戻す
  try {
    $stmt = $pdo->prepare(insql);
    $stmt->execute([$_SESSION['sid'], $_POST['name']]);
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }

endp:
  header('HTTP/1.1 303 See Other');
  header('Location: ./');
  exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sign in</title>
</head>
<body>
<h1>サインイン</h1>
<p>
もしくは<a href="./signup.php">サインアップ</a>
</p>
<form METHOD="POST">
<fieldset>
<legend>サインイン</legend>
<div>
<label for="name">名前<label>
<input type="text" name="name" id="name" required>
</div>
<div>
<label for="pass">パスワード:</label>
<input type="password" name="pass" id="pass" required>
</div>
<input type="submit">
</fieldset>
</form>
</body>
</html>
/var/www/html/bbs/signup.php
<?php
const dsn = 'mysql:host=localhost;dbname=bbs';
const dbuser = 'bbsuser';
const dbpass = 'bbspasswd';
const insql = 'INSERT INTO user (name,hash,sid) VALUES (:name,:hash,:sid)';
const cksql = 'SELECT COUNT(*) FROM user WHERE name=?';
const sname = 'BBSLOGIN';

# POST (= 新規登録情報を受信) だったら
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (empty($_POST['name']) || empty($_POST['pass']))
    goto endp;

  # 同じユーザ名は使われていないか?
  try {
    $pdo = new PDO(dsn, dbuser, dbpass);
    $stmt = $pdo->prepare(cksql);
    $stmt->execute([$_POST['name']]);
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }
  if ($stmt->fetchColumn() !== '0') {
    $name = $_POST['name'];
    die("The name is used. ($name)");
  }
  
  # ログイン処理をしながら新規登録
  try {
    $stmt = $pdo->prepare(insql);
    $stmt->bindValue(':name', $_POST['name'], PDO::PARAM_STR);
    $stmt->bindValue(':hash',
      password_hash($_POST['pass'], PASSWORD_DEFAULT), PDO::PARAM_STR);
    $stmt->bindValue(':sid', $sid = exec('uuidgen'), PDO::PARAM_STR);
    $stmt->execute();
  } catch (PDOException $e) {
    $error = $e->getMessage();
    die("Something Wrong ($error)");
  }

  # セッション開始
  session_name(sname);
  session_start();
  session_regenerate_id(true);
  $_SESSION['sid'] = $sid;

endp:
  header('HTTP/1.1 303 See Other');
  header('Location: ./');
  exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>サインアップ</title>
</head>
<body>
<h1>サインアップ</h1>
<p>
もしくは<a href="./signin.php">サインイン</a>
</p>
<form METHOD="POST">
<fieldset>
<legend>Sign up</legend>
<div>
<label for="name">名前:</label>
<input type="text" name="name" id="name" required>
</div>
<div>
<label for="pass">パスワード:</label>
<input type="password" name="pass" id="pass" required>
</div>
<input type="submit">
</fieldset>
</form>
</body>
</html>
/var/www/html/bbs/signout.php
<?php
const dsn = 'mysql:host=localhost;dbname=bbs';
const dbuser = 'bbsuser';
const dbpass = 'bbspasswd';
const insql = 'UPDATE user SET sid=NULL WHERE sid=?';
const sname = 'BBSLOGIN';

session_name(sname);
session_start();

if (empty($_SESSION['sid']))
  goto endp;

# セッション ID を無効化
try {
  $pdo = new PDO(dsn, dbuser, dbpass);
  $stmt = $pdo->prepare(insql);
  $stmt->execute([$_SESSION['sid']]);
} catch (PDOException $e) {
  $error = $e->getMessage();
  die("Something Wrong ($error)");
}

# クッキーを削除
$_SESSION = array();
if (ini_get('session.use_cookies')) {
  $params = session_get_cookie_params();
  setcookie(session_name(), '', time() - 42000, $params['path'],
    $params['domain'], $params['secure'], $params['http_only']);
}

# セッションの破棄
session_destroy();

endp:
header('HTTP/1.1 303 See Other');
header('Location: ./');

おわりに

おわりです。

セキュリティの流儀がわからないので詳しい人はご教授ください。