利用者の認証
利用者をログインさせたりログアウトさせたり、その権限を確かめたりするしくみなしで済むウェブアプリケーションはほとんどありません。この章では次のことを扱います。
- 利用者のログインとログアウト
- 独自の認証器
例では Nette\Security\Userクラスのオブジェクトを使います。これは今の利用者を表し、dependency
injectionで渡してもらえます。プレゼンターでは $user = $this->getUser()
を呼ぶだけです。
認証
認証とは利用者のログイン、つまり利用者の身元を確かめる過程のことです。利用者はふつうユーザー名とパスワードで自分を名乗ります。確認はいわゆる認証器が行います。ログインに失敗すると
Nette\Security\AuthenticationException が投げられます。
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('入力されたユーザー名かパスワードが正しくありません。');
}
利用者をログアウトさせるにはこうします。
$user->logout();
そして利用者がログインしているかを調べるには次のようにします。
echo $user->isLoggedIn() ? 'yes' : 'no';
とても簡単でしょう。そして安全にまつわる面はすべて Nette が引き受けます。
プレゼンターでは startup()
メソッドでログインを確かめ、ログインしていない利用者をログインのページへリダイレクトできます。
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
有効期限
利用者のログインは保管場所の期限とともに切れます。それはふつうセッションです(セッションの有効期限の設定をご覧ください)。とはいえ、利用者がログアウトさせられるまでのもっと短い時間も決められます。そのために
setExpiration() メソッドを login()
の前に呼びます。引数には相対の時間の文字列を渡します。
// 30 分の無操作でログインが切れます
$user->setExpiration('30 minutes');
// 決めた有効期限を取り消します
$user->setExpiration(null);
$user->getLogoutReason()
メソッドは、利用者が時間切れでログアウトさせられたのかを教えてくれます。定数
Nette\Security\User::LogoutInactivity(時間の制限が切れた)か
User::LogoutManual(logout() メソッドが呼ばれた)を返します。
認証器
これはログインの資格情報、ふつうはユーザー名とパスワードを確かめるオブジェクトです。ごく簡単な形が Nette\Security\SimpleAuthenticatorクラスで、設定で定められます。
security:
users:
# ユーザー名: パスワード
johndoe: 'secret123'
kathy: 'evenmoresecretpassword'
平文のパスワードの代わりに、そのハッシュも書けます。設定をご覧ください。
この解は試しの用途に向いています。ここでは、ログインの資格情報をデータベースのテーブルと照らし合わせる認証器の作り方をお見せします。
認証器は Nette\Security\Authenticatorインターフェースを
authenticate() メソッドとともに実装するオブジェクトです。その仕事は身元を返すか、Nette\Security\AuthenticationException
を投げるかです。状況をより細かく見分けるために、エラーのコードを指定することもできます。Authenticator::IdentityNotFound
か Authenticator::InvalidCredential です。
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('User not found.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Invalid password.');
}
return new SimpleIdentity(
$row->id,
$row->role, // またはロールの配列
['name' => $row->username],
);
}
}
MyAuthenticator クラスは Nette Database
Explorerでデータベースとやり取りし、users のテーブルを扱います。そこでは
username の列に利用者のログイン名が、password の列にパスワードのハッシュが入っています。名前とパスワードを確かめたあと、利用者の
ID、そのロール(テーブルの role の列。これはのちほど詳しく扱います)、そして追加のデータの配列(ここではユーザー名)を含む身元を返します。
この認証器を設定に、DI コンテナのサービスとして足します。
services:
- MyAuthenticator
$onLoggedIn、$onLoggedOut のイベント
Nette\Security\User オブジェクトにはイベント $onLoggedIn と $onLoggedOut
があるので、ログインが成功したあとや、利用者がログアウトしたあとに呼ばれるコールバックを足せます。
$user->onLoggedIn[] = function () {
// 利用者がたった今ログインしました
};
身元
身元は認証器が返す利用者の情報のひとまとまりで、そのあとセッションに保存され、$user->getIdentity()
で取り出せます。これで
ID、ロール、そのほかの利用者のデータを、認証器で渡したとおりに得られます。
$user->getIdentity()->getId();
// 近道の $user->getId() も使えます
$user->getIdentity()->getRoles();
// 利用者のデータはプロパティとして触れられます
// MyAuthenticator で渡したユーザー名
$user->getIdentity()->name;
大事なのは、$user->logout()
でログアウトしても身元は消されず、まだ使えるということです。ですから利用者が身元を持っていても、ログインしているとは限りません。身元をはっきり消したいなら、logout(true)
を呼んで利用者をログアウトさせます。
おかげで、どの利用者がコンピュータの前にいるかをなお推し量れて、たとえばネットショップでその人に合わせた提案を見せられます。ただしその人の個人の情報は、ログインしたあとにだけ見せられます。
logout(true) で呼び出しごとに身元を消すほかに、$persistIdentity
のプロパティで身元を残す働きをまるごと切れます。false
にすると、ログアウトのたびに、そして期限切れのときに身元は捨てられるので、getIdentity()
は null
を返すようになります。身元を残せるかは保管場所にもよります。クッキーの保管場所はログアウト後に身元を残せません。いつもクッキーを消すからです。
身元は Nette\Security\IIdentityインターフェースを実装するオブジェクトです。既定の実装は Nette\Security\SimpleIdentityです。そして先ほど触れたとおりセッションに保たれるので、たとえばログイン中の利用者のひとりのロールを変えても、その人がもう一度ログインするまでは古いデータが身元に残ります。
ログインした利用者の保管場所
利用者についての 2 つの基本の情報、つまりログインしているかどうかと身元は、ふつうセッションで運ばれます。これは変えられます。この情報の保管を受け持つのは
Nette\Security\UserStorage インターフェースを実装するオブジェクトです。標準の実装は 2
つあります。データをセッションで運ぶ Nette\Bridges\SecurityHttp\SessionStorage
と、クッキーで運ぶ CookieStorage です。保管場所は security ›
authenticationの設定でとても手軽に選んで整えられます。
さらに、身元の保存(sleep)と読み戻し(wakeup)がどう進むかにも手を入れられます。必要なのは、認証器が
Nette\Security\IdentityHandler
のインターフェースを実装することだけです。sleepIdentity()
メソッドは身元が保管場所へ書かれる前に、wakeupIdentity()
は読み出されたあとに呼ばれます。これらのメソッドは身元の中身を変えたり、返す新しいオブジェクトに置き換えたりできます。wakeupIdentity()
メソッドは null
を返すこともでき、その場合は利用者がログアウトさせられます。このインターフェースは
getGuestIdentity() メソッドも宣言しています。訪問者の身元をご覧ください。
例として、セッションから読み込んだ直後に身元のロールを更新するにはどうするか、というよくある問いへの解をお見せします。wakeupIdentity()
メソッドで、たとえばデータベースから今のロールを身元へ渡します。
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// ここでログイン後に保管場所へ書く前の身元を変えられますが、
// 今は必要ありません
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// 身元のロールを更新します
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
public function getGuestIdentity(): ?IIdentity
{
// ここでは訪問者の身元は使いません
return null;
}
ではクッキーをもとにした保管場所に戻りましょう。これを使うと、利用者がログインできて、しかもセッションをまったく必要としないウェブサイトを作れます。つまりディスクへ書く必要がありません。あなたが今読んでいるこのウェブサイトも、フォーラムも含めてこのように動いています。この場合、IdentityHandler
の実装は欠かせません。クッキーには、ログインした利用者を表す無作為なトークンだけを保存します。
まず設定で security › authentication › storage: cookie
として、必要な保管場所を指定します。
データベースには authtoken の列を作り、利用者ごとにまったく無作為で、一意で、推測できない十分な長さの文字列(少なくとも
13 文字)を入れます。CookieStorage はクッキーに $identity->getId()
の値だけを運ぶので、sleepIdentity() ではもとの身元を、ID に authtoken
を持つ代理の身元に置き換えます。逆に wakeupIdentity() メソッドでは、その authtoken
をもとにデータベースから身元全体を読み出します。
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// パスワードを確かめます
// ...
// データベースのすべてのデータを持つ身元を返します
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// ID に authtoken を持つ代理の身元を返します
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// 代理の身元を、authenticate() と同じ完全な身元に置き換えます
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
public function getGuestIdentity(): ?IIdentity
{
// ここでは訪問者の身元は使いません
return null;
}
}
訪問者の身元
ログインしていない訪問者にも身元があると便利なことがあります。たとえば既定のロールの一そろいや何かのデータを与えるためです。認証器が
IdentityHandler を実装していれば、getGuestIdentity()
メソッドでそれを与えられ、誰もログインしていないときにいつでも使われます。すると
getIdentity()、getId()、getRoles() はそれに落ちるので、訪問者はただの
guest のロールではなく自分のロールを持てます。訪問者の身元が要らないなら
null を返します。
public function getGuestIdentity(): ?IIdentity
{
return new SimpleIdentity('guest', ['guest'], ['name' => 'Guest']);
}
訪問者の身元は保管場所には決して保存されず、ログインするといつでも置き換えられます。
独立した複数のログイン
ひとつのウェブサイト、ひとつのセッションの中で、独立した複数の利用者が同時にログインすることもできます。たとえば管理画面とウェブサイトの公開の部分で別々の認証をしたいなら、それぞれに一意の名前空間を設定するだけです。
$user->getStorage()->setNamespace('backend');
その部分に属するすべての場所で、いつも名前空間を設定するのを忘れないことが大事です。プレゼンターを使っているなら、その部分の共通の祖先、ふつうは BasePresenter で名前空間を設定します。それは checkRequirements()メソッドを広げて行います。
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
ひとつのリクエストの途中で(認証の状態がすでに読まれたあとに)名前空間を切り替えると、User
オブジェクトは前の名前空間から蓄えた状態をまだ持っています。その場合は
refreshStorage() を呼んで、蓄えを捨てて新しい名前空間から読み直させます。
$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // 新しい名前空間から状態を読み直します
複数の認証器
アプリケーションを独立したログインを持つ部分に分けると、ふつうは違う認証器も必要になります。とはいえ
Authenticator を実装するクラスを 2 つサービスの設定に登録すると、Nette はどちらを
Nette\Security\User
オブジェクトに自動的に割り当てるべきか分からず、エラーを見せます。ですから認証器のオートワイヤリングを制限して、たとえば
FrontAuthenticator
のような特定のクラスが求められたときにだけ働くようにしなければなりません。それは
autowired: self を選んで実現します。
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
User オブジェクトの認証器は login()メソッドを呼ぶ前に設定するので、ふつうはログインさせるフォームのコードの中で行います。
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};