【PHP】データベースを使わずにログイン機能を作ってみる

# はじめに
「**データベースを構築して運用するほどではないけど、ログイン機能を実装して簡単なシステムを作りたい**」と思ったことはありませんか？
実装したいシステムのユーザ数がそれほど多くない（～数十程度）なら、データベースを作らなくても、「**BASIC認証のしくみ**」×「**テキストベースのユーザーデータ保存**」を使えば、最低限のセキュリティを考慮したログイン機能が作れます。
セキュリティ対策を万全にするにはデータベースを使うのが通例だと思いますので、実装したいシステムとユーザーが扱うデータに機密情報が含まれるようなものは作らないでください。

PHPでログイン機能を作るときデータベースと連携してユーザー情報を扱う方法はたくさん紹介されているので、あえてデータベースを使わずにログイン機能を実装する方法を紹介します。

# どんな方向けの記事か

- PHPが動作するレンタルサーバを契約し、**自前のHTMLを作成してWebサイトを運営している方**
- サーバのデータベースに利用制限があるがログイン機能を実装したい方
- データベース構築の知識があまりない方
- さくっと簡単なログイン機能を作ってすぐに運用したい方
- PHPのプログラム方法や機能をこれから学びたい方

運営しているWebサイトがHTTPSで暗号化通信できる状態になっていることが望ましいです。
レンタルサーバ標準機能のHTTPS化で構いません。

# どんなシステムを想定しているか

- サークルや団体（～数十人規模まで）のイベントの予約／キャンセルシステム
- などなど

企業や法人に発注するほどの大きなシステムでなければ、自分で作って運用してみるのが良いかもしれません。


# 参考にさせていただいたサイト

- 【コピペで使える】ログイン機能の簡単実装サンプル（PHP／MySQL）
https://tadworks.jp/archives/1147

こちらではMySQLを使った簡単なログイン機能が紹介されています。当記事ではこのプログラムをベースとしながら、データベースを使わない方法で実装してみます。


# 前提知識

## 1. BASIC認証

BASIC認証とは、特定のフォルダ、ファイルに対してアクセス制限をかけるための最も基本的な認証方法です。
レンタルサーバを契約していれば、「アクセス制限設定」などの設定で簡単に設定できます。
BASIC認証設定の根幹となる仕組みとして、「**BASIC認証を行うための特別なファイルを配置し、そこにアクセスできるユーザ名とそのパスワードを指定する**」ことでこの機能が発動します。

FTPのアカウント情報が盗まれてはファイルにアクセスできるので意味がなくなりますが、FTP情報が盗まれないことを前提にすれば、**HTTPで最低限そのフォルダの情報にアクセスされない状況が作れます**。

本記事のログイン機能に使う情報の保存には、この仕組みを使います。

具体的には、**アクセス制限をかけたフォルダの中にユーザデータをテキストとして保存します**。
ユーザ側はそのファイルにはアクセスできませんが、後述するPHPを動かす**サーバ側はアクセスできる**ことを利用しています。


## 2. PHP

PHPは、サーバーサイドで動作するプログラムを記述するためのプログラミング言語です。

Webサイトを運営する際に、PHPを使わなければHTMLやCSS、Javascriptを使う場合がほとんどだと思いますが、これらはすべてクライアントサイドで、Webページを閲覧しているブラウザでの振る舞いを決定します。

PHPはサーバで動かすことができるため、ユーザに応じて内容を変えるような動的なシステムを作るのには必須で、**できることが格段に広がります**。

サーバが実行するプログラムなので、**サーバから見れば当然自分の場所にあるファイルにアクセスすることができます**。
アクセス制限をかけているフォルダはあくまでクライアントに対しての制限なので、サーバならアクセスできるのです。
この仕組みを利用します。


PHPにはログイン機能が備わっており、「セッション」の技術を利用することで簡単に実装できます。
セッションの機能についてはこちらの記事で詳しく紹介されています。

PHPでセッションを使う方法【初心者向け】 - TechAcademyマガジン
https://techacademy.jp/magazine/4970#ta-toc-1



# ログイン機能の全体像

基本的な考え方はデータベースを使う場合と変わりませんが、そのデータベースの部分をアクセス制限のかかったフォルダに置き換えて実装してしまおう、というのがコンセプトです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/ab2ca1fc-e704-7862-c4d6-32ef46b997e2.png)


# 実装してみよう

お待たせしました。ここで実装に入っていきたいと思います。


## 0. アクセス制限を設定する

- FTPでサーバに接続し、ファイルのアップロード／ダウンロードができる状態にします。
- まず、好きなフォルダに対してアクセス制限の設定をします。そのフォルダにデータを保存していくことになります。
    - 手動で以下のような「.htaccess」ファイルを配置してください。
    - ここではルートディレクトリ内の「data」ディレクトリをアクセス制限し、そのディレクトリ内にアップロードします。

```:.htaccess
AuthUserFile "/[サーバのパス]/data/.htpasswd"
AuthName "Basic Authentication"
AuthType BASIC
require valid-user
<Files ~ "^.(htpasswd|htaccess)"$>
deny from all
</Files>
```

上記のファイルで、アクセス可能なユーザ名とパスワードを格納したファイルとして`"/[サーバのパス]/data/.htpasswd"`と指定しています。
現在は`.htpasswd`というファイルを用意していないため、**認証画面でどのようなユーザ名・パスワードを入力してもサーバエラー(500)が発生します**。
**つまり、この状態で誰もアクセスできない状況が作れている、とも言えます。**

このままでもよいと思われますが、サーバ側のエラーをそのままにしておくのはスマートではないため、推測されにくいパスワードを暗号化したものを`.htpasswd`に記述し、`.htaccess`と同じディレクトリにアップロードします。

- 推測されにくい複雑なパスワードの生成は以下のツールを使用すると便利です。 
    - LUFTTOOLS: https://www.luft.co.jp/cgi/randam.php
- `.htpasswd`用のパスワードの暗号化は以下のツールを使用すると便利です。 
    - LUFTTOOLS: https://www.luft.co.jp/cgi/htpasswd.php

```:.htpasswdの記述方法
[ユーザ名]:[暗号化したパスワード]
複数ユーザを指定する場合は改行して追記

```

私の環境では、ユーザ名もランダムな文字列とし以下のようにしました。

```:.htpasswd
uiNxX3tSmsDt:WGbjBnzP5H/i.

```

アップロード後のファイルの配置は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/076f55f1-ec02-176c-6cf5-29f26d93ec15.png)




## 1. ユーザ情報を記録したテキストファイルを用意する

データベースではないので、どのようなデータ構造で情報を保存するかは自由に決定することができます。

ここでは、「[ユーザーネーム(IDとして利用)].txt」という名前のファイルに、パスワードをMD5ハッシュに通した文字列を記録しておくことにします。パスワードを平文で保存するのは脆弱性があるため、ハッシュ関数に通し、これを記録しておきます。
ハッシュ関数の仕組みについては割愛します。

MD5の計算には以下のようなツールを用いるのが速いです。
ＭＤ５ハッシュ計算ツール：
https://phpspot.net/php/pg%EF%BC%AD%EF%BC%A4%EF%BC%95%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E8%A8%88%E7%AE%97%E3%83%84%E3%83%BC%E3%83%AB.html


ここではわかりやすくするためにユーザーネームを「username」、平文のパスワードを「password」にします。
他に保存しておきたいデータがあれば、行を追加して記述します。
適当な例として、団体内の役職（会計）を記述してみます。

```text:username.txt
5f4dcc3b5aa765d61d8327deb882cf99
会計
```

このテキストファイルを、アクセス制限を掛けたフォルダにアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/ccf225bd-27a6-c190-d6c5-1a42e5aa2d75.png)


## 2. ログインフォームを作る(index.php)

まずこのWebサイトにアクセスしてきたユーザに対してログインフォームを表示させるindex.phpを作成します。

以下のプログラムをご自分のHTMLに組み込み、index.phpとしてください。
何かログインに問題が生じた時のメッセージを表示できるようにするため、その変数「$message」も定義しておきます。

ファイルの文字コードはすべてUTF-8を想定しています。

**2021/12/23追記：脆弱性のご指摘をいただき、HTMLエスケープとディレクトリトラバーサルの対処を行いました。**

- HTMLエスケープについて：echo関数でそのまま文字列をHTML化してしまうと、**その中に含まれるタグやスクリプトもそのまま処理されるため、任意のコードが実行されてしまう危険があります**。これにかかわる重要な文字（例：<, >, "など）をエスケープして無害化します。

- ディレクトリトラバーサル：ファイル名を格納している変数に"../"といった親ディレクトリを示す文字列を入れることで、**サーバ内の任意のファイルにアクセスできてしまうこと**を指します。今回はbasename関数を利用して、親ディレクトリを指定する文字列をなくしファイル名のみに変換します。




```php:index.php(プログラム部分のみ)
<?php
    // HTMLエスケープ関数 <- 取得した任意のHTMLコードを実行する脆弱性に対処します
    function to_html($str){
        return htmlspecialchars($str, ENT_QUOTES|ENT_HTML5, "UTF-8");
    }

    $message = "";
    if($_GET['message']){
        $message = $_GET['message'];
    }
    
    //======= ログイン状況の確認 ==============
    session_start();

    if(isset($_SESSION['data'])){
        $login = true;
        $data = $_SESSION['data'];
        $username = $data['username'];
        $role = $data['role'];
    } else {
        $login = false;
    }

    // すでにログインしている場合、メインページに遷移
    if($login){
        header('Location: main.php');
        exit;
    }

    //======= ログイン状況の確認END ==============

?>

<div class="form-body">
    <p><?php echo to_html($message); ?></p>
    <form class="form" action="./login.php" method="post" enctype="multipart/form-data">

        ■ユーザーネーム
        <input type="name" name="name" class="form-control" id="name" placeholder="ユーザーネームを入力">

        ■パスワード
        <input type="password" name="password" class="form-control" id="password" placeholder="パスワードを入力"> 
        
        <div style="text-align: right; padding: 5px;">
            <button type="submit" value="submit" class="btn btn-primary">ログイン</button>
        </div>
    </form>
</div>

```

ログインしている場合、`$_SESSION['data']`に値が格納されているため、この有無を判定することでログインしているかしていないかを場合分けしています。

以下のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/009f5193-2b9a-294f-0a26-a915ec9044ef.png)



## 3. ログイン処理プログラムを作る(login.php)

ここでは、index.phpから送られてきたユーザーネームとパスワードを照らし合わせ、実際にログイン処理を行います。
照合できなければ、ユーザーネームかパスワードが間違っている旨をindex.phpに返し、再びログインページを表示させます。
照合できれば、セッション変数にログイン情報を与えメインページ(main.php)に遷移します。

バックグラウンドで処理を行うプログラムのため、以下のPHPプログラムのみをlogin.phpとして保存すればよいかと思います。

```php:login.php
<?php
$username = $_POST["name"];
$password = $_POST["password"];

// ディレクトリトラバーサル対策
$username = basename($username);

if(!file_exists("data/".$username.".txt")){ // ユーザーデータがない場合
	header('Location: index.php?message=ユーザーネームまたはパスワードが間違っています。');
	exit;

}else{ // ユーザーデータがある場合

    //ユーザーデータの取得
    $user_data = fopen('data/'.$username.'.txt', "r");
    
    $password_hash = trim(fgets($user_data)); // trim関数：文字列の最初と最後にあるホワイトスペースを取り除く
    $role = trim(fgets($user_data));
    // $password_hash = str_replace(array("\r\n","\r","\n"), '', fgets($user_data));
    // $role = str_replace(array("\r\n","\r","\n"), '', fgets($user_data));
    
    fclose($user_data);

    // パスワードの照合
    if(md5($password) != $password_hash){ // パスワードが一致しない場合
        header('Location: index.php?message=ユーザーネームまたはパスワードが間違っています。');
        exit;
    }

    // ログインOK
    session_start();
    $data = [];
    $data['username'] = $username;
    $data['role'] = $role;

    $_SESSION['data'] = $data; // ユーザ情報をセッション変数に格納
    header('Location: main.php'); // メインページに遷移
    exit;
}
?>

```

このプログラムでは、パスワードの照合に成功した時にセッションを開始し、ユーザーネームと役職をセッション情報に格納しておきます。
セッションが終了するまではこの情報が維持されるため、表示するファイルをまたぐことができます。



## 4. ログイン後のメインページを作る(main.php)

ここでは、ログインに成功した後に表示するメインページを作ります。
実装したいシステムに応じて作成してください。

ログインしていない状態でアクセスができないよう、PHPプログラムの初めにログイン状態を判定し、ログインしていなければログインページに飛ぶようにしています。

ベースはindex.phpに記載したログイン判定と同じです。作成したメインページ用のHTMLに以下のプログラムを組み込み、main.phpとしてください。

```php:main.php(プログラム部分のみ)

<?php
    // HTMLエスケープ関数 <- 取得した任意のHTMLコードを実行する脆弱性に対処します
    function to_html($str){
        return htmlspecialchars($str, ENT_QUOTES|ENT_HTML5, "UTF-8");
    }
    
    //======= ログイン状況の確認 ==============
    session_start();

    if(isset($_SESSION['data'])){
        $login = true;
        $data = $_SESSION['data'];
        $username = $data['username'];
        $role = $data['role'];
    } else {
        $login = false;
    }

    // ログインしていない場合、ログインページに遷移
    if(!$login){
    header('Location: index.php?message=セッションの期限が終了したか、不正なアクセスです。');
        exit;
    }

    //======= ログイン状況の確認END ==============

?>

<div class="body">
    <h1 class="title">メインページ</h1>
    <p>ようこそ、<?php echo to_html($username); ?>さん(役職：<?php echo to_html($role); ?>)</p>
    <div style="text-align: right; padding: 5px;">
        <a href="./logout.php" class="btn btn-primary">ログアウト</a>
    </div>
</div>

```

この例では、ログインに成功すると以下のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/7fbe9dfd-baa2-9141-e9a9-fdb0f96a0a4e.png)



## 5. ログアウト処理プログラムを作る(logout.php)

ログインしているユーザがログアウトできるように以下のプログラムを使用します。

```php:logout.php
<?php
session_start();
$_SESSION = array(); // セッション情報をクリアする
session_destroy(); // セッションを終了する
header('Location: index.php?message=ログアウトしました。'); // ログインページに遷移する
?>
```

**2021/12/23 追記**
もしアクセス制限のあるディレクトリに接続しようとしてBASIC認証情報が保存されてしまった際のために、header関数を以下に変更すると強制的にBASIC認証情報がクリアされ、ログインページに戻ります。

```php
header('Location: https://user@hogehoge.com/');
```

＠の前に適当なユーザ名を入れることで認証に失敗し、保存されている認証情報を破棄し強制的に再認証させる挙動になります。

以上の4つのPHPプログラムだけで基本的なログイン機能を実装できました。
実際にindex.phpにアクセスし、挙動を確認してみてください。


# まとめ

本記事ではデータベースを使わずに、テキストベースの情報を利用してログイン機能を実装する方法を紹介しました。
レンタルサーバの契約によってデータベースを使えない状況でも、この実装を使えば小規模なシステムを作ることができます。
ログイン後に表示するmain.php以外にも、＜ログイン状況の確認＞部分のプログラムを書いておけばページをまたぐことができます。

今回は簡単のためにパスワードハッシュと役職のみを記載しましたが、そのほかの情報を同じように書いておけばユーザごとに情報を管理して動的にページを変化させることができます。
ユーザ数が多かったり、格納する情報に応じて複雑な処理を行わなければいけない場合はデータベースを導入することをお勧めします。

私もこれからデータベースについて勉強したいと思います。