✅
【ハンズオン】PHPでシンプルなRemember Me(自動ログイン)を実装する
初めに
認証周りが分からないというコンプレックスを克服すべく、下記の2記事に続き、Remember Me(自動ログイン機能)を実装してみました。
どなたかの役に立てば幸いです。
実装の流れ
- ログインフォームで「ログイン情報を記憶する」チェックボックスを置く
- メールアドレスとパスワードがusersテーブルのレコードに合致したら、そのユーザーとしてログイン
- もし「ログイン情報を記憶する」にチェックが入っていたら、下記の処理をし、ログイン
- ランダムな文字列のトークンを生成し、cookieにremember_tokenとして保存
- 上記トークンを暗号化したものを、usersテーブルのremember_tokenカラムに保存
- マイページへアクセス時、下記の通りログインチェック
- セッションにユーザーIDがあれば、ログイン済みとしてマイページを表示
- ユーザーIDは無いがcookieにremember_tokenがあれば、その値をusersテーブルのremember_tokenに持つユーザーとしてログインし、マイページへ
- 上記どちらも当てはまらなければ、未ログインとしてログインフォームを表示
- マイページのログアウトボタンを押下時、usersテーブルのremember_tokenカラムをNULLに更新しログアウト
ファイル構成
.
├─ database.php
├─ login.php
├─ logout.php
├─ show_login_form.php
├─ show_mypage.php
└─ views
├── login_form.php
└── mypage.php
使用するテーブル
usersテーブル
カラム名 | 備考 |
---|---|
id | 主キー |
register_token | 仮登録時に使用。ユーザーを一意に識別する |
register_token_sent_at | 仮登録の有効期限管理に使用 |
register_token_verified_at | 本登録時に更新 |
password | |
remember_token | remember me実装に使用。ユーザーを一意に識別する |
status | enum型。tentative:仮登録、puvlic:本登録 |
created_at | |
updated_at |
作成したDDLはこちら(MySQLを想定)
CREATE TABLE `users` (
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`email` VARCHAR(50) UNIQUE NOT NULL,
`register_token` VARCHAR(80),
`register_token_sent_at` DATETIME,
`register_token_verified_at` DATETIME,
`password` VARCHAR(80),
`remember_token` VARCHAR(80),
`status` ENUM('tentative', 'public') NOT NULL DEFAULT 'tentative',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
流し込み用SQL
INSERT INTO `users` (`id`, `email`, `register_token`, `register_token_sent_at`, `register_token_verified_at`, `password`, `status`, `created_at`, `updated_at`)
VALUES
(1,'example@example.com','45085a3ce32c6623c4159dc3e202007c6ef26d0a8201ab9802426a35a0bc2474','2022-03-27 12:21:41','2022-03-27 12:21:58','$2y$10$BmxkPc6uMmRyWvgvpUUs1OOlSuplcyLb6FzQEooh1LHS/aPUt6no2','public','2022-03-27 12:21:41','2022-03-27 16:38:58');
1. メールアドレスとパスワードがusersテーブルのレコードに合致したら、そのユーザーとしてログイン
show_login_form.php
<?php
session_start();
// formに埋め込むcsrf tokenの生成
if (empty($_SESSION['_csrf_token'])) {
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
}
// 本登録フォームを読み込む
require_once './views/login_form.php';
フォーム部分(formタグ内を抜粋)
views/login_form.php
<p>ログイン</p>
<form action="login.php" method="POST">
<input type="hidden" name="_csrf_token" value="<?= $_SESSION['_csrf_token']; ?>">
<label>メールアドレス
<input type="email" name="email">
</label>
<br>
<label>パスワード
<input type="password" name="password">
</label>
<br>
<label>ログイン情報を記憶する
<input type="checkbox" name="remember_me">
</label>
<br>
<button type="submit">ログイン</button>
</form>
2. メールアドレスがusersテーブルに登録済みなら3へ、未登録ならメール送信完了画面を表示して終了
login.php
<?php
session_start();
require_once './database.php';
$pdo = getPdo();
$request = filter_input_array(INPUT_POST);
// csrf tokenが正しければOK
if (
empty($request['_csrf_token'])
|| empty($_SESSION['_csrf_token'])
|| $request['_csrf_token'] !== $_SESSION['_csrf_token']
) {
exit('不正なリクエストです');
}
// 本来はここでメールアドレスとパスワードのバリデーションをする
// 入力されたメールアドレスに合致するユーザーを取得
$sql = 'SELECT * FROM users WHERE `email` = :email AND `status` = :status';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':email', $request['email'], \PDO::PARAM_STR);
$stmt->bindValue(':status', 'public', \PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch(\PDO::FETCH_OBJ);
// 「メールアドレスが間違っている」のようにどちらが間違っているのか表示すると、
// 別人のアカウントでログインしようとする悪意あるユーザーに、
// 「メールアドレスが間違っている」「パスワードは間違っていない」という情報を与えてしまうので、明示しない
if (
!$user
|| !password_verify($request['password'], $user->password)
) {
exit('登録情報が間違っています。');
}
// セッションが有効な間はログイン済みとなる
$_SESSION['user_id'] = $user->id;
// 「ログイン情報を記憶する」をチェックしていなければ、このままマイページへ
if (empty($request['remember_me'])) {
header('Location: ./show_mypage.php');
exit();
}
コード内に出てくるdatabase.phpの中身はこちらです。
database.php
database.php
<?php
function getPdo()
{
$dsn = 'mysql:host=localhost;dbname=zenn;charset=utf8mb4';
$options = [
\PDO::ATTR_PERSISTENT => true,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
];
try {
return new \PDO($dsn, 'root', 'root', $options);
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
3. もし「ログイン情報を記憶する」にチェックが入っていたら、Remember Me(自動ログイン)の処理をする
login.php(※2.のファイルの続き)
// ランダムな文字列のtokenを生成
$rememberToken = bin2hex(random_bytes(32));
// 暗号化したremember tokenを保存
$sql = 'UPDATE users SET `remember_token` = :remember_token WHERE `email` = :email';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':remember_token', md5($rememberToken), \PDO::PARAM_STR);
$stmt->bindValue(':email', $request['email'], \PDO::PARAM_STR);
$stmt->execute();
// cookieのオプション
$options = [
'expires' => time() + 60 * 60 * 24 * 365, // cookieの有効期限を1年間に設定
'path' => '/', // 有効範囲を「ドメイン配下全て」に設定
'httponly' => true // HTTPを通してのみcookieにアクセス可能(JavaScriptからのアクセスは不可となる)
];
setcookie('remember_token', $rememberToken, $options);
header('Location: ./show_mypage.php');
exit();
4. マイページへアクセス時、2通りのログインチェックをし、マイページを表示
show_mypage.php(※3.のファイルの続き)
<?php
session_start();
require_once './database.php';
$pdo = getPdo();
// (1) セッションにユーザーIDが保存されている場合
if (isset($_SESSION['user_id'])) {
// セッションに保存されたユーザーIDに合致するユーザーを取得
$sql = 'SELECT * FROM users WHERE `id` = :id AND `status` = :status';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $_SESSION['user_id'], \PDO::PARAM_INT);
$stmt->bindValue(':status', 'public', \PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch(\PDO::FETCH_OBJ);
// (2) セッションのユーザーIDは破棄されているが、cookieにremember_tokenが保存されている場合
} else if (isset($_COOKIE['remember_token'])) {
// usersテーブルに保存されている remember_token は md5() で暗号化されているので、同様に暗号化した token をWHERE句に指定する
$sql = 'SELECT * FROM users WHERE `remember_token` = :remember_token AND `status` = :status';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':remember_token', md5($_COOKIE['remember_token']), \PDO::PARAM_STR);
$stmt->bindValue(':status', 'public', \PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch(\PDO::FETCH_OBJ);
}
// ユーザーが一致しなければ、ログインフォームへ
if (empty($user)) {
header('Location: ./show_login_form.php');
exit();
}
// ユーザーIDをセッションに保存してログイン済みとする
$_SESSION['user_id'] = $user->id;
// formに埋め込むcsrf tokenの生成
if (empty($_SESSION['_csrf_token'])) {
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
}
require_once './views/mypage.php';
マイページ(formタグ内を抜粋)
mypage.php
<p><?= $user->email ?>さんのマイページ</p>
<form action="logout.php" method="POST">
<input type="hidden" name="_csrf_token" value="<?= $_SESSION['_csrf_token']; ?>">
<button type="submit">ログアウトはこちら</button>
</form>
cookieを確認するため、検証ツール → Applicationタブ → Storage →Cookies と確認してみましょう。
(1) セッションにユーザーIDが保存されている場合
cookieに保存されているのはセッションクッキーのみです。
1度ブラウザを終了し、ブラウザを再度起動して直接マイページへ行ってみましょう。
セッションはブラウザを終了すると破棄されるため、ログアウト状態となりログインフォームが表示されます。
(2) セッションのユーザーIDは破棄されているが、cookieにremember_tokenが保存されている場合
cookieにはセッションクッキーに加え、remember_tokenが保存されています。
cookieは有効期限を1年に設定したため、ブラウザを閉じても破棄されません。
ブラウザを終了してから再度起動しマイページへ行くと、remmeber_tokenを取得し、値が一致するユーザーでログインされます。
※session_startを宣言しているので、PHPSESSID自体は発行されています
5. マイページのログアウトボタンを押下時、usersテーブルのremember_tokenカラムをNULLに更新しログアウト
logout.php
<?php
session_start();
require_once './database.php';
$pdo = getPdo();
$request = filter_input_array(INPUT_POST);
// csrf tokenが正しければOK
if (
empty($request['_csrf_token'])
|| empty($_SESSION['_csrf_token'])
|| $request['_csrf_token'] !== $_SESSION['_csrf_token']
) {
exit('不正なリクエストです');
}
// remember_tokenをNULLにする
$sql = 'UPDATE users SET remember_token = NULL WHERE `id` = :id';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $_SESSION['user_id'], \PDO::PARAM_INT);
$stmt->execute();
// remeber_tokenをcookieから削除
setcookie('remember_token', '', time() - 6000, '/');
// 以下、セッションの削除処理
// セッション変数を初期化(メモリから削除するため)
$_SESSION = [];
// セッションクッキーを削除
setcookie('PHPSESSID', '', time() - 6000, '/');
// セッションファイル(セッションの実データ)を削除
session_destroy();
// ログインフォームへ
header('Location: ./show_login_form.php');
exit();
最後までお読みいただき、ありがとうございました!
Discussion