前回から新しく「PHP修羅の道シリーズ」の記事を上げていくことになりました。
このシリーズでは自己学習として作成したアプリに関する記事などPHP関連の記事をシリーズとして投稿していく予定です。
普段学習に使用している教材はこちらになります。
今回はシリーズの第二弾として「つぶやきアプリ」に関する記事となります。
前回の「簡単な計算機アプリ」に比べ自分の中では難易度が爆上がりしました。というのも前回はHTML/PHPだけを使った簡単な実装でしたが、今回はHTML/CSS/PHPを用いてより機能的により実際のアプリを模倣したアプリになっています。
そして何よりDBの活用によるプログラムの幅の広がりや、セキュリティを意識したコードという点において未熟ながらも大変勉強になりました。
私自身、知識も経験も浅いPHP初学者であるため間違いなどもあるかもしれませんが、そこはとりあえず学習のために動くアプリを作れたということで何卒ご勘弁をお願いします(笑)
アプリの作成
作成したアプリ
それぞれの機能は後ほど紹介しますが、ログイン前のタイムラインとログイン後のタイムライン画像と操作映像のご紹介をします。
ログイン前
ログイン後
デモ映像
完成イメージ
「つぶやきアプリ」の作成に伴い初めに本家PC版Twitterをイメージしました。完成アプリで背景色を紺色にしているのはそのためです。
CSSでデザインをいじったのは全ての処理をコーディングした最終工程なので、当初予定には含めていませんでした。アプリを作成する中で愛着が湧いてきたので勉強ついでにいじってみたという感じです。
【基本画面】
・トップ画面
・タイムライン1(ログイン前)
・ユーザー登録画面
・ログイン画面
・タイムライン2(ログイン後)
・その他:登録・ログイン・つぶやきなどの確認と完了画面等
【追加機能】
・CSSデザイン
・他サイトの表示
・Basic認証
その他の細かい詳細機能は画面ごとに紹介していきます。
トップ画面の実装
これは最後に追加した画面なので別に今回のアプリには必要のないものでしたが、せっかくなのでHTMLとCSSで遊びたいなということで追加することにしました。
Basic認証
このページではBasic認証を採用しています。
WEBサイトにアクセする時に制限をかけることができる最も簡易的な認証方式のこと
<?php
$pathFile = file(__DIR__ . '/path.txt');
$path = explode(':',$pathFile[0]);
//パスワード(暗号化)
(password_hash($path[1], PASSWORD_BCRYPT));
?>
index.php
HTMLを書き始める前にこちらのphpコードを先頭に記述しています。こうすることでこのindex.phpと同じ階層にあるログイン用のパスを記述したファイルを読み込んで$pathFileにそのパスを格納します。
その後、password_hash()関数にPHP7から推奨となったPASSWORD_BCRYPTを設定することで、 CRYPT_BLOWFISHアルゴリズムで新たなパスワードハッシュを作ります。例えば、「12345」というパスワードにするとアルゴリズムに沿って、「$2y$10$YkEhx.5W8b8NcBUKWfF3WOmo/O.EL.raDvuRz0On3NkinIzE7Y8si」
という文字列に変換されます。
ここで必要になる隠しファイルが「.htaccess」「.htpasswd」の二つです。「.〇〇〇」というファイル名にすることで隠しファイルとして扱うことができます。
①.htaccess
ファイルには下記内容を記述し、最終行は改行した状態で保存します。
AuthType Basic //ベーシック認証をかける
AuthName "IDとパスワードを入力してください"
AuthUserFile /Applications/・・・/.htpasswd //パスのファイルのありかを示す
require valid-user //認証したいユーザーだけが中に入ることができる
②.htpasswd
ファイルには下記内容を記述し、最終行は改行した状態で保存します。
id:password //入力するidとパスワードを設定
タイムライン1(ログイン前)の実装
ログイン前のタイムラインではつぶやきを投稿することはできませんが、タイムラインの閲覧は可能です。また、ログイン前はユーザーの登録画面とログイン画面への画面遷移ができます。ツイッターのロゴを押下するとトップページに戻れる遊び心も加えてみました(笑)
画面構成としては縦に3分割しており、両サイドには他WEBサイトの表示とYouTubeの動画を配置しています。
クリックジャッキング対策
クリックジャッキング(クリックジャック攻撃)とは、透明表示機能などによって見えないページを準備し、そのページ上にあるボタンをユーザにクリックさせることによって、思わぬ損害を与えるような攻撃のこと
header()関数のX-FRAME-OPTIONSに「DENY」を設定することで、フレームでのページの読み込みの一切を禁止することができます。
DBからデータを取得
データベースへのアクセスはPDO(PHP Data Objects)を利用します。PDOはPHPからデータベースへ簡単にアクセスするための拡張モジュールです。
SQL文(SELECT)
順番は前後しますが、DBに登録される情報は「ユーザー情報」と「つぶやき情報」になります。ここからデータを取り出していきます。
usersテーブル
messagesテーブル
$selectData = $db -> prepare
(
'SELECT
messages.id,
messages.user_id,
messages.text,
messages.created_date,
users.account,
users.NAME
FROM
`messages`
INNER JOIN
`users`
ON
messages.user_id = users.id
ORDER BY id
DESC'
);
top.php
内部結合することでユーザーとつぶやきを紐付け、ユーザーごとのつぶやき情報を取得します。また、並び順を降順にすることでタイムラインに表示させる時に常に画面上部に最新のつぶやきが表示されるようになります。
取得した情報はforeach文で回すことで、DBに登録されているつぶやきとユーザー情報を全て表示することができます。
今回は表示させたいデータとHTMLタグを下記のように一つの変数にぶち込んでいます。
$display_messages[] =
'<br>'.
'<font color="#08ffc8">'.
$name.'@'.$account.
'</font>'.
'<br>'.
$message_text.
'<br>'.
'<font color="#5bd1d7">'.
$created_date.
'</font>'.
'<br>';
top.php
なぜこうしたかというと、HTMLを記述した際の可読性を上げるためです。
<div class="main">
<div class="message-block">
<?php foreach($display_messages as $display_message) :?>
<div class="border-bottom border-light p-2">
<?php echo nl2br($display_message); ?>
</div>
<?php endforeach ;?>
</div>
</div><!--main-->
top.php
PHPではこのようにHTMLとの親和性が高いので途中で挿入することが可能です。その際に先ほどのデータを変数を入れるだけで簡潔に表示することができるようになります。
iframe(インラインフレーム)
インラインフレーム要素を使うことで現在のHTMLページに他のページを埋め込むことができます。
左側のサイドバー
<div class="left-sidebar">
<iframe class= "frame_center" src="https://www.famitsu.com" width="100%" height="1500"></iframe>
</div>
top.php
右側のサイドバー
<div class="right-sidebar">
<div class="video">
<iframe width="560" height="315" src="https://www.youtube.com/embed/hBl5r_uxbSY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<div class="video2">
<iframe width="560" height="315" src="https://www.youtube.com/embed/Jgm4D0n4gxk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<div class="video3">
<iframe width="560" height="315" src="https://www.youtube.com/embed/DtE1nxSb4d4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div><!--right-sidebar-->
top.php
ユーザー登録画面の実装
ユーザー登録画面では5項目の入力項目がありますが、自己紹介フォーム以外は入力必須項目に設定しています。パスワードに関しては入力なしの場合「パスワードは必須です」と表示され、入力していても4文字未満であれば「パスワードは4文字以上で入力してください」と表示されます。
条件を満たす入力が完了すると確認画面へ画面が切り替わります。
このあと登録を押下することで登録完了画面が表示されます。
DBへデータを登録
ユーザー登録画面ではユーザー情報の登録を行うため、DBのusersテーブルへ情報を登録する処理を行います。
SQL文自体は単純なINSERT文で、フォームから入力された情報を$_POSTで受け取ることで各カラムへと登録ができるようになります。
//ユーザー登録フォームで入力された値を受け取る
$signup_name = $_POST['signup'];
$name = $_POST["name"];
$account = $_POST["account"];
$password = $_POST["password"];
$email = $_POST["email"];
$description = $_POST["description"];
database.php
また、このphpファイルのtry文内ではヒアドキュメントを使用しています。
<<<_区切り文字_ 文字列 _区切り文字_とすることで、区切り文字内の文字列をそのまま変数に格納することができる。また、文字列中の変数はその値に置き換えることができる。
このヒアドキュメントを利用することにより、HTML内で7行分のコードを変数一つで表示でき、可読性が高くなります。
<div class="message-block">
<?php echo $finish_register?>
<br>
<p><?php echo $msg; ?></p>
</div>
database.php
同じphpファイル内で画面切り替え
「ユーザー登録」「登録内容確認」「登録完了」は同じPHPファイル内で処理されており、画面の切り替えは$pageFlagという変数に格納する数値を切り替えることで実現させています。
//画面切り替え
$pageFlag = 0;
if(!empty($_POST['btn_confirm']) && empty($errors)){
$pageFlag = 1;
}
if(!empty($_POST['btn_register'])){
$pageFlag = 2;
}
signup.php
HTMLのname属性を「btn_confirm」に設定している「確認する」ボタンを押下することでフォームに入力された情報がPOSTされ、「$_POST」によってデータを受け取ることが可能になります。ボタンが押され、かつエラーがなければ確認画面に表示を切り替えることができます。
<!--確認ボタン-->
<button type="submit" class="btn btn-primary" name="btn_confirm" value="確認する">確認する</button>
signup.php
エラーメッセージは別のvalidation.phpで定義しているので、ファイルの先頭で定義しているファイルを読み込みこのsignup.php内で使えるようにしています。
//validation.php
//ユーザー登録時のエラーメッセージ
function validation($request){
$errors = [];
if(empty($request['name']) || 20 < mb_strlen($request['name'])){
$errors[] = '「名前」は必須です。20文字以内で入力してください。';
}
if(empty($request['account']) || 20 < strlen($request['account'])){
$errors[] = '「アカウント名」は必須です。20文字以内で入力してください。';
}
if(empty($request['password'])){
$errors[] = '「パスワード」は必須です。';
}else{
if(0 < strlen($request['password']) && 4 > strlen($request['password'])){
$errors[] = '「パスワード」は4文字以上で入力してください。';
}
if(20 < strlen($request['password'])){
$errors[] = '「パスワード」は20文字以内で入力してください。';
}
}
if(empty($request['email']) || !filter_var($request['email'], FILTER_VALIDATE_EMAIL)){
$errors[] = '「メールアドレス」は必須です。正しい形式で入力してください。';
}
return $errors;
}
validation.php
クロスサイトスクリプティング(XSS)攻撃対策
クロスサイトスクリプティング(XSS)とは、Webサイトの脆弱性を利用し、記述言語であるHTMLに悪質なスクリプトを埋め込む攻撃のこと
JavaScriptなどのスクリプトが実行されないように、スクリプトの構成に必要な「&,<,>,”,’」の5文字の特殊文字がそのまま画面に表示されてしまうようにエスケープ(置換)してスクリプトのサニタイジング(無害化)を行います。
//クロスサイトスクリプティング攻撃対策
function h($str){
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
$_POSTで受け取った値を下記のような感じで全てサニタイズします。
<input type="text" class="form-control" name="name" id="signup-name" placeholder="マリオ" value="<?php if(!empty($_POST['name'])){echo h($_POST['name']);} ?>">
・https://www.shadan-kun.com/waf/xss/
・https://www.nttpc.co.jp/column/security/cross_site_scripting.html
クロスサイトリクエストフォージェリ(CSRF)対策
攻撃者は自身が直接攻撃対象サーバへアクセスすることなく攻撃対象のWebアプリケーションに任意の処理を行わせることにより、誘導された一般ユーザが攻撃対象サーバへの不正なリクエストを送信した攻撃者として誤認識させる攻撃のこと
誘導されたユーザーはいたずら的な書き込みや、大量の不正な書き込み、不正サイトへの誘導など本人が意図しない攻撃を行ってしまうことになります。
今回のつぶやきアプリでは「GETやPOSTという1度きりのやりとりではなく、SESSIONという有効期限までの間は情報を保持する方法で対策」をしてみました。
<!--csrfTokenがなければ変数に代入する-->
<?php
if(!isset($_SESSION['csrfToken'])){
$csrfToken = bin2hex(random_bytes(32));
$_SESSION['csrfToken'] = $csrfToken;
}
$token = $_SESSION['csrfToken'];
?>
signup.php
bin2hex(random_bytes(32))によって安全なバイト列を16進数に変換し、それをSESSION[‘csrfToken’]に格納します。
<input type="hidden" name="csrf" value="<?php echo $token; ?>">
signup.php
この生成したtokenをhidden属性で画面に値を持たせ、切り替わった後の画面ではそのtokenを$_POSTでその値を受け取ります。こうすることで、画面が切り替わる前と切り替わった後でのcsrfTokenを比較することで、意図する画面への切り替えが行われているかの判定が実現できます。
もしここでcsrfTokenが一致しなかった場合は、同じ見た目の悪意ある全く別のページに飛ばされてしまうことになるので画面が切り替わらないように制御しています。
【画面が切り替わる前】
・$_SESSION[‘csrfToken’]で保持しているcsrfToken
【画面が切り替わった後】
・$_POSTで受け取ったcsrfToken
<!--トークンが一致しているか判定する-->
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) :?>
<!--確認画面表示などの処理-->
<?php endif; ?>
signup.php
判定がfalseであれば「if~endif」で囲まれた内側の処理は行われないので、確認画面も表示されないという仕組みです。
ログイン画面の実装
ログイン画面では先ほどユーザー登録画面で登録した「アカウント名かメールアドレス」と「パスワード」でログインが可能となります。
この際、登録されてないアカウントでログインしようとすると画面が切り替わり、再度ログイン画面で情報を入力するように促します。
ログインに成功すると、ログイン完了画面へと移り、ログイン後のタイムライン画面へと進むことができるボタンが表示されます。
基本的にはログイン画面も他の画面と同様のセキュリティ対策や画面切り替えを行っているため、ぞの部分での処理内容はそこまで大きくは変わっていません。
SQLインジェクション攻撃対策
SQLクエリの一部に不正なパラメータを含めることで意図しない操作を実行させる攻撃手法のこと。
プリペアードステートメント
SQL文で値がいつでも変更できるように、変更する箇所のみ命令文を作る仕組みのことです。この仕組みを使うことで、内部的に自動でエスケープ処理を施してくれためセキュリティ面でも安全に使用することができます。
PHPではPDOオブジェクトを使ってprepare()を使用することで可能となります。
プレースホルダ
プレースホルダは、「値をバインドさせる仕組み」のことです。
bindvalue()という関数を使用することでSQL文内の目的の箇所に値を挿入することができます。これにより、実行段階で目的のパラメータを与えることができるので、値をSQL文とは切り離して安全に活用することができます。
このつぶやきアプリではフォームから受け取った値をバインドした箇所に挿入し、execute()でプリペアードステートメントを実行しています。
また、fetchAll()メソッドにPDO::FETCH_ASSOCのフェッチスタイルを設定することで文字列キーによる配列として行全体を取得しています。
タイムライン2(ログイン後)の実装
タイムライン2(ログイン後)の画面では、タイムライン1(ログイン前)画面に加えてつぶやき投稿欄を追加しています。ここで呟くことによってタイムラインに最新のつぶやきが表示されていきます。
ログイン後のタイムラインでは、ログイン成功のご褒美にみんな大好きひろゆきさんの動画を観ることができるようになります(笑)
つぶやき欄で好きなつぶやき(140文字以内)を打ち込み、確認するボタンを押下します。
つぶやき内容の確認画面では先ほど打ち込んだつぶやきが表示されるのでつぶやくボタンを押下することでタイムラインにつぶやきが反映されます。
また、つぶやき入力画面に戻っても先ほど入力した内容は保持されているため、つぶやきの一部を書き直したい場合でも手間なく編集できる工夫をしています。
投稿されるとこのようにタイムラインの上部につぶやきが表示されるようになります。
最後に
今回の記事は結構内容が分厚めの記事となってしまいましたが、どうでしたでしょうか?
自己学習の一環で作成し始めたアプリですがいざ完成まで色々試行錯誤してみるとかなり勉強になり、本で読むだけ、動画で勉強するだけよりも遥かに高い効果が得られることがわかりました。
コードを書いていくことで頻発するエラーの解消法を探って原因を突き止めたり、このコードだとこんな処理になるというのがリアルタイムでわかるので、時間はかかりますが非常に自分の一つの経験として積み重なることを実感しています。
そして、このようなブログ記事を通して誰かに対して説明する難しさも実感できました。コードを書いて動作させる以上に理解をしておかないと言葉にできませんからね。
今回はアプリ作成を通して、HTML/CSS/PHPでのコーディング知識だけでなく、エラーの自力解決、セキュリティ対策、git操作、PDOに関するDB操作関連の知識、SESSIONの理解など幅広く学習することができ、やってよかったと思う次第です。
次はLaravelなどのフレームワークを活用したアプリケーションを作成して、より実務向きの技術を勉強していきたいなと思っています。
ちなみにですが今回作成したつぶやきアプリ内でマリオがつぶやいている映画化は実話なので公開を楽しみに待ちたいと思います(笑)
では、また👋