認証

Nette はサイトに認証を作る方法を差し出しますが、特定のやり方を押し付けはしません。どう作るかは完全にあなた次第です。Nette には Nette\Security\Authenticator のインターフェースがあり、必要なメソッドは authenticate ひとつだけで、好きなやり方で利用者を確かめられます。

利用者を認証する方法はたくさんあります。もっともよくあるのはパスワードによる認証(利用者が名前かメールとパスワードを入れます)ですが、ほかのやり方もあります。サイトによっては「Facebook でログイン」のボタンや、Google/Twitter/GitHub 経由のログインを見たことがあるでしょう。Nette ならどんなログインの方法も使えますし、組み合わせることもできます。すべてあなたが決めることです。

ふつうは自分の認証器を書きます。とはいえこの単純なブログでは、設定ファイルに書いたユーザー名とパスワードでログインする、組み込みの認証器を使います。試すにはこれで十分です。設定ファイル config/common.neon に次の security の区画を足しましょう。

security:
	users:
		admin: secret  # ユーザー 'admin'、パスワード 'secret'

Nette はこの認証器のサービスを DI コンテナに自動的に作ります。

ログインのフォーム

認証の準備ができたので、ログインのための画面を作る必要があります。SignPresenter という新しいプレゼンターを作りましょう。これは次のことをします。

  • ログインのフォームを表示する(ユーザー名とパスワードの項目付き)
  • フォームが送信されたら利用者を認証する
  • ログアウトの手段を用意する

まずログインのフォームから始めます。プレゼンターでのフォームの働きはもうご存じです。SignPresenter を作り、createComponentSignInForm メソッドを書きましょう。次のようになります。

<?php
namespace App\Presentation\Sign;

use Nette;
use Nette\Application\UI\Form;

final class SignPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentSignInForm(): Form
	{
		$form = new Form;
		$form->addText('username', 'ユーザー名:')
			->setRequired('ユーザー名を入力してください。');

		$form->addPassword('password', 'パスワード:')
			->setRequired('パスワードを入力してください。');

		$form->addSubmit('send', 'ログイン');

		$form->onSuccess[] = $this->signInFormSucceeded(...);
		return $form;
	}
}

ユーザー名とパスワードの項目があります。

テンプレート

このフォームは in.latte のテンプレートで描かれます。

{block content}
<h1 n:block=title>ログイン</h1>

{control signInForm}

ログインのコールバック

次に、利用者をログインさせるコールバックのメソッドを足します。これはフォームが正しく送信されたすぐあとに呼ばれます。

このコールバックは、利用者が入れたユーザー名とパスワードを取って認証器へ渡すだけです。ログインに成功したら、トップページへリダイレクトします。

private function signInFormSucceeded(Form $form, \stdClass $data): void
{
	try {
		$this->getUser()->login($data->username, $data->password);
		$this->redirect('Home:');

	} catch (Nette\Security\AuthenticationException $e) {
		$form->addError('ユーザー名かパスワードが正しくありません。');
	}
}

User::login()メソッドは、ユーザー名とパスワードが設定ファイルの資格情報と合わなければ例外を投げます。ご存じのとおり、それは Tracy の赤い画面か、本番モードならサーバーのエラーのメッセージになります。それは望むところではありません。ですから例外を捕まえて、利用者にやさしいきれいなエラーのメッセージをフォームに足します。

フォームにエラーが起きると、フォームのあるページが描き直され、ユーザー名かパスワードが違うことを知らせるメッセージがフォームの上に現れます。

プレゼンターを守る

記事を足したり編集したりするフォーム、つまり EditPresenter で定義したものを守ります。目指すのは、ログインしていない利用者がそのページに触れられないようにすることです。

startup() メソッドを作ります。これはプレゼンターのライフサイクルのいちばん初めにすぐ動きます。このメソッドが、ログインしていない利用者をログインのフォームへリダイレクトします。

public function startup(): void
{
	parent::startup();

	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

リンクを隠す

権限のない利用者はもう createedit のページを見られませんが、そこを指すリンクはまだ見えています。それも隠すべきです。そうしたリンクのひとつは app/Presentation/Home/default.latte のテンプレートにあり、ログインした利用者にだけ見えるべきです。

これは n:if という n:属性 で隠せます。条件が false なら、<a> のタグは中身も含めてまるごと表示されません。

<a n:href="Edit:create" n:if="$user->isLoggedIn()">記事を作る</a>

これは次の書き方の近道です(tag-if と混同しないでください)。

{if $user->isLoggedIn()}<a n:href="Edit:create">記事を作る</a>{/if}

app/Presentation/Post/show.latte のテンプレートの編集のリンクも、同じやり方で隠してください。

ログインへのリンク

ところでログインのページへはどうやって行くのでしょうか。そこへ導くリンクがありません。@layout.latte のテンプレートに足しましょう。ふさわしい場所を探してみてください。ほとんどどこでもかまいません。

...
<ul class="navig">
	<li><a n:href="Home:">ホーム</a></li>
	{if $user->isLoggedIn()}
		<li><a n:href="Sign:out">ログアウト</a></li>
	{else}
		<li><a n:href="Sign:in">ログイン</a></li>
	{/if}
</ul>
...

利用者がログインしていなければ「ログイン」のリンクが表示されます。そうでなければ「ログアウト」のリンクが見えます。このアクションも SignPresenter に足します。

ログアウトのあとすぐに利用者をリダイレクトするので、テンプレートは要りません。ログアウトのアクションは次のようになります。

public function actionOut(): void
{
	$this->getUser()->logout();
	$this->flashMessage('ログアウトしました。');
	$this->redirect('Home:');
}

logout() メソッドを呼び、そのあと利用者に確認のメッセージを見せるだけです。

まとめ

利用者をログインさせたりログアウトさせたりするリンクができました。認証には組み込みの認証器を使い、ログインの資格情報は設定ファイルに置きました。これは単純な試しのアプリケーションだからです。編集のフォームも守り、ログインした利用者だけが記事を足したり編集したりできるようにしました。

利用者のログイン認可については、こちらで詳しく読めます。