仕事でやや変則的な認証処理を作る事になったので、Laravelでスキャッフォルドされる会員登録周りの実装がどうなっているのかを調べてみました。
該当箇所のコードを引用しつつ、コメントを付与する形で解説しています。
なお、会員登録周りをカスタマイズする方法を解説している記事ではないので注意してください。
あくまでLaravel内部でどういう処理が行われているのかを調べた記事になります。
調査に使用したLaravelのバージョンは5.7.22
です。
関連記事
Laravelの実装調査 ~ログイン編・リメンバーログインもあるよ~
Laravelの実装調査 ~パスワードリセット編~
今回は会員登録編という事で、以下2ケースをまとめています。
- 会員登録
- メールアドレス確認
それでは見ていきましょう。
会員登録
ポストされたメールアドレスやパスワードをDBに保存する処理です。
以後、これらの情報を用いてログインできる様になります。
ルーティング
Illuminate\Routing\Router.php 1156行目あたり
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
バリデーション
App\Http\Controllers\Auth\RegisterController.php 49行目あたり
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
]);
}
処理概要
Illuminate\Foundation\Auth\RegistersUsers.php 29行目あたり
public function register(Request $request)
{
// バリデーション
$this->validator($request->all())->validate();
// ユーザ情報をDBに保存 & 会員登録イベント発火
event(new Registered($user = $this->create($request->all())));
// ログイン
$this->guard()->login($user);
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
DBへの保存
ユーザ情報をDBに保存する処理です。
パスワードはbcrypt
でハッシュ化して保存しています。
App\Http\Controllers\Auth\RegisterController.php 64行目あたり
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
// パスワードはハッシュ化して保存する(次のコードスニペットを参照)
'password' => Hash::make($data['password']),
]);
}
password_hash
関数を使ってハッシュを生成している。
Illuminate\Hashing\BcryptHasher.php 45行目あたり
public function make($value, array $options = [])
{
// ハッシュを生成
$hash = password_hash($value, PASSWORD_BCRYPT, [
'cost' => $this->cost($options),
]);
if ($hash === false) {
throw new RuntimeException('Bcrypt hashing not supported.');
}
return $hash;
}
確認メール送信
メールアドレスが本人の所有している物かどうかを確認するために、確認メールを送信する事もできる。
確認メールは会員登録後のイベント経由で送信される。(EventServiceProvider.phpで定義されている)
Illuminate\Auth\MustVerifyEmail.php 34行目あたり
public function sendEmailVerificationNotification()
{
$this->notify(new Notifications\VerifyEmail);
}
確認リンク生成(署名付きURL)
確認メールに埋め込むための署名付きのリンクを生成する。
ルートパラメータとしてユーザID
を付与している。
Illuminate\Auth\Notifications\VerifyEmail.php 59行目あたり
protected function verificationUrl($notifiable)
{
// 一定期間で無効になる署名URLを生成する
return URL::temporarySignedRoute(
'verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
);
}
ユーザID
と有効期限
を含めたURL(絶対パス)をhash_hmac
に掛けて署名(signature
)を生成しているのが分かる。
Illuminate\Routing\UrlGenerator.php 319行目あたり
public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true)
{
$parameters = $this->formatParameters($parameters);
if ($expiration) {
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}
ksort($parameters);
$key = call_user_func($this->keyResolver);
// 署名付きURLの生成
return $this->route($name, $parameters + [
// 署名の生成
'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
], $absolute);
}
以下の様なURLが生成される
https://.../email/verify/1?expires=1523128426&signature=63b4744eb3aea943ddd7411d92bf2d8cea528dda011e29a9da128fac5c7e8ab7
メールアドレス確認
メールアドレスが本人の所有している物かどうかを確認するための処理
会員登録時に送ったメールに記載されているリンクに遷移すると当該処理が実行される。
ルーティング
Illuminate\Routing\Router.php 1191行目あたり
$this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
$this->get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
$this->get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
ミドルウェア
上記のルートに対して付与されているミドルウェア
signed
ミドルウェアで署名を検証しリンクが改ざんされていない事をチェックする
App\Http\Controllers\Auth\VerificationController.php 37行目あたり
$this->middleware('auth');
$this->middleware('signed')->only('verify'); // 署名付きURLの検証
$this->middleware('throttle:6,1')->only('verify', 'resend'); // 1分間に6回までアクセスを許可する
処理概要
Illuminate\Foundation\Auth\VerifiesEmails.php 33行目あたり
public function verify(Request $request)
{
// 署名付きURLに付与されたユーザIDとログイン済みユーザIDが一致するか
if ($request->route('id') != $request->user()->getKey()) {
throw new AuthorizationException;
}
// メール確認済みならリダイレクト
if ($request->user()->hasVerifiedEmail()) {
return redirect($this->redirectPath());
}
// メール確認済みとする
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect($this->redirectPath())->with('verified', true);
}
メール確認済みかどうかの保存先
MustVerifyEmail
トレイトを実装したモデルのemail_verified_at
プロパティにタイムスタンプ形式で保持される。(Eloquent Modelのプロパティなのでカラム名と同義)
Illuminate\Auth\MustVerifyEmail.php 22行目あたり
public function markEmailAsVerified()
{
return $this->forceFill([
'email_verified_at' => $this->freshTimestamp(),
])->save();
}
署名の確認処理
signed
ミドルウェアの処理にあたります。
リンクが改ざんされていないかを調べるために署名を生成し比較する
Illuminate\Routing\UrlGenerator.php 357行目あたり
public function hasValidSignature(Request $request, $absolute = true)
{
$url = $absolute ? $request->url() : '/'.$request->path();
// ①:リクエストされたURLから'signature'を省いたURLを取得する
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
// ②:有効期限を取得する
$expires = $request->query('expires');
// ③:①で生成したURLから署名を生成
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
// ④:③で生成した署名とリクエストに付与されていた署名を比較する
return hash_equals($signature, (string) $request->query('signature', '')) &&
// 有効期限を過ぎていないか調べる
! ($expires && Carbon::now()->getTimestamp() > $expires);
}
メール確認が済んでいるかのチェック
verified
ミドルウェアを使用する
今回登場したクラス達
App
└ Http
└ Controllers
└ Auth
├ RegisterController
└ VerificationController
Illuminate
├ Auth
│ ├ MustVerifyEmail
│ └ Notifications
│ └ VerifyEmail
├ Foundation
│ └ Auth
│ ├ RegistersUsers
│ └ VerifiesEmails
├ Hashing
│ └ BcryptHasher
└ Routing
├ Router
└ UrlGenerator
おわりに
以上、会員登録周りの実装調査でした。
何かしら新しい発見があったなら幸いです。
私としては、会員登録後の確認メールで署名付きリンクが使われているのは目から鱗でした。
署名付きリンクであればDBに一時データを持つ必要がないのがスマートでいいと思います。