[laravel]인증기능 커스텀

라라벨은 기본적으로 인증기능을 지원하지만 상황에 따라 커스텀을 해야하는 경우가 있다.

목표

  • 라라벨인증 기능은 사용자가 이용하고, 커스텀한 인증은 관리자가 이용할 수 있게 한다.

목차

  1. 프로젝트 생성
  2. 라라벨인증 기능 설치
  3. 모델 및 마이그레이션 생성
  4. 모델 migrate
  5. 가드, 프로바이더, 패스워드 지정
  6. 미들웨어 생성 및 등록
  7. 라우트 생성
  8. 컨트롤러, 뷰 생성

1. 프로젝트 생성

laravel new blog
cd ./blog
php artisan key:generate
composer install

2. 라라벨 인증기능 설치

라라벨인증을 설치하는 이유

  1. 사용자는 기본인증을 사용하기로 함
  2. 커스텀인증은 처음부터 구현하는것 보다 기본인증을 복사해서 사용하는것이 시간을 절약할 수 있다.
composer require laravel/ui --dev
npm install
npm run dev

3. 모델 및 마이그레이션 생성

기본인증에 사용되는 User모델이 있지만 관리자는 새로운 테이블로 관리하려고하기 때문에 새로운 모델과 마이그레이션을 생성해야 한다.

하지만 메일인증에 사용하는 password_resets 테이블은 분리하지 않고 공통으로 사용하기로 한다.

php artisan make:model Admin -m

‘-m’ 옵션을 사용하면 마이그레이션도 같이 생성된다.

Admin 모델 정의

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Admin extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

마이그레이션 파일을 열어 테이블 스키마를 생성한다.

    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admins');
    }

4. 모델 migrate

migrate 명령어를 통해 테이블을 생성해주자

php artisan migrate

5. 가드, 프로바이더, 패스워드 지정

config/auth.php 파일을 열어 생성한 Admin 모델에 대해 가드, 프로바이더, 패스워드를 추가해준다.

    'guards' => [

        ...

        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

    'providers' => [
 
        ...

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],
    ],

    'passwords' => [

        ...

        'admin' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],
  • guards : 매요청마다 어떻게 인증할 것인가
  • providers : 저장소에서 사용자를 어떻게 찾을 것인가
  • passwords : 사용자 테이블에 대한 암호를 재설정하는데 사용하는 브로커

6. 미들웨어 생성과 등록

라우트를 보호할 목적으로 미들웨어를 생성한다. 기본인증에 사용되는 미들웨어 클래스 Authenticate , RedirectIfAuthenticated 를 동일하게 구현할 예정이다.

미들웨어 생성

php artisan make:middleware AdminAuthenticate
php artisan make:middleware AdminRedirectIfAuthenticated
  • AdminAuthenticate : 인증된 사용자가 아니면 로그인 페이지로 리다이렉트
  • AdminRedirectIfAuthenticated : 인증되지 않은 요청만 허용, 로그인된 사용자라면 홈으로 리다이렉트

AdminAuthenticate 미들웨어는 상속받은Illuminate\Auth\Middleware\Authenticate 클래스의 redirectTo메서드를 오버라이드 해서 리다이렉트 경로를 지정해준다.

#AdminAuthenticate.php

    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('admin.login');
        }
    }

AdminRedirectIfAuthenticated 는 클래스를 상속받기 않기 때문에 handle매서드를 지정해주면 된다. Auth파사드를 사용하기 위해 Illuminate\Support\Facades\Auth 네임스페이스 사용한다.

#AdminRedirectIfAuthenticated.php

use Illuminate\Support\Facades\Auth;

    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            return redirect('/admin/home');
        }

        return $next($request);
    }

정의한 미들웨어를 app/http/kernel.php 에 등록한다.

    protected $routeMiddleware = [
        
        ...

        'admin.auth' => \App\Http\Middleware\AdminAuthenticate::class,
        'admin.guest' =>\App\Http\Middleware\AdminRedirectIfAuthenticated::class,
    ];

7. 라우트 생성

라라벨 인증에 사용되는 라우트는 아래와 같다. 아래의 기본인증 라우트는 그대로 두고 새로 만든 admin전용 라우트를 생성한다.

  • 기본인증 라우트
MethodURINameActionMiddleware
GET|HEADloginloginApp\Http\Controllers\Auth\LoginController@showLoginFormweb,guest
POSTlogin App\Http\Controllers\Auth\LoginController@loginweb,guest
POSTlogoutlogoutApp\Http\Controllers\Auth\LoginController@logoutweb
GET|HEADregisterregisterApp\Http\Controllers\Auth\RegisterController@showRegistrationFormweb,guest
POSTregister App\Http\Controllers\Auth\RegisterController@registerweb,guest
GET|HEADpassword/confirmpassword.confirmApp\Http\Controllers\Auth\ConfirmPasswordController@showConfirmFormweb,auth
POSTpassword/conform App\Http\Controllers\Auth\ConfirmPasswordController@confirmweb,auth
GET|HEADpassword/resetpassword.requestApp\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestFormweb
POSTpassword/emailpassword.emailApp\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmailweb
GET|HEADpassword/reset/{token}password.resetApp\Http\Controllers\Auth\ResetPasswordController@showResetFormweb
POSTpassword/resetpassword.updateApp\Http\Controllers\Auth\ResetPasswordController@resetweb
GET|HEADemail/verifyverification.noticeApp\Http\Controllers\Auth\VerificationController@showweb,auth
POSTemail/resendverification.resendApp\Http\Controllers\Auth\VerificationController@resendweb,auth,throttle:6,1
GET|HEADemail/verify/{id}/{hash}verification.verifyApp\Http\Controllers\Auth\VerificationController@verifyweb,auth,signed,throttle:6,1
GET|HEADhomehomeApp\Http\Controllers\HomeController@indexweb,auth

위 라우트는 web.php에 정의되어 있지 않고 Auth::routes(); 로 등록되어 있다. 기본인증 라우트를 확인 하려면 /vendor/laravel/ui/src/AuthRouteMethods.php를 확인하면 된다.

  • admin 라우트
MethodURINameActionMiddleware
GET|HEADadmin/loginadmin.loginApp\Http\Controllers\Admin\LoginController@showLoginFormweb,admin.guest
POSTadmin/login App\Http\Controllers\Admin\ LoginController@loginweb,admin.guest
POSTadmin/logoutadmin.logoutApp\Http\Controllers\Admin\ LoginController@logoutweb
GET|HEADadmin/registeradmin.registerApp\Http\Controllers\Admin\ RegisterController@showRegistrationFormweb,admin.guest
POSTadmin/register App\Http\Controllers\Admin\ RegisterController@registerweb,admin.guest
GET|HEADadmin/password/confirmadmin.password.confirmApp\Http\Controllers\Admin\ ConfirmPasswordController@showConfirmFormweb,admin.auth
POSTadmin/password/conform App\Http\Controllers\Admin\ ConfirmPasswordController@confirmweb,admin.auth
GET|HEADadmin/password/resetadmin.password.requestApp\Http\Controllers\Admin\ ForgotPasswordController@showLinkRequestFormweb
POSTadmin/password/emailadmin.password.emailApp\Http\Controllers\Admin\ ForgotPasswordController@sendResetLinkEmailweb
GET|HEADadmin/password/reset/{token}admin.password.resetApp\Http\Controllers\Admin\ ResetPasswordController@showResetFormweb
POSTadmin/password/resetadmin.password.updateApp\Http\Controllers\Admin\ResetPasswordController@resetweb
GET|HEADadmin/email/verifyadmin.verification.noticeApp\Http\Controllers\Admin\VerificationController@showweb,admin.auth
POSTadmin/email/resendadmin.verification.resendApp\Http\Controllers\Admin\VerificationController@resendweb,admin.auth,throttle:6,1
GET|HEADadmin/email/verify/{id}/{hash}admin.verification.verifyApp\Http\Controllers\Admin\VerificationController@verifyweb,admin.auth,signed,throttle:6,1
GET|HEADadmin/homeadmin.homeApp\Http\Controllers\Admin\HomeController@indexweb,admin.auth
#routes/web.php

Route::namespace('Admin')->group(function(){
    Route::prefix('admin')->group(function(){
        //로그인
        Route::get('login', 'LoginController@showLoginForm')->name('admin.login');
        Route::post('login', 'LoginController@login');
        Route::post('logout', 'LoginController@logout')->name('admin.logout');
        //회원가입
        Route::get('register', 'RegisterController@showRegistrationForm')->name('admin.register');
        Route::post('register', 'RegisterController@register');
Route::get('home', 'HomeController@index')->name('admin.home');
        //패스워드확인
        Route::get('password/confirm', 'ConfirmPasswordController@showConfirmForm')->name('admin.password.confirm');
        Route::post('password/confirm', 'ConfirmPasswordController@confirm');
        //패스워드리셋
        Route::get('password/reset', 'ForgotPasswordController@showLinkRequestForm')->name('admin.password.request');
        Route::post('password/email', 'ForgotPasswordController@sendResetLinkEmail')->name('admin.password.email');
        Route::get('password/reset/{token}', 'ResetPasswordController@showResetForm')->name('admin.password.reset');
        Route::post('password/reset', 'ResetPasswordController@reset')->name('admin.password.update');
        //이메일인증 확인
        Route::get('email/verify', 'VerificationController@show')->name('admin.verification.notice');
        Route::post('email/resend', 'VerificationController@resend')->name('admin.verification.resend');
        Route::get('email/verify/{id}/{hash}', 'VerificationController@verify')->name('admin.verification.verify');
    });
});

8. 컨트롤러 및 뷰생성

(1) 컨트롤러 뼈대 만들기

app/Http/Controller 아래에 Admin 폴더를 생성
app/Http/Controller/Auth 하위 파일을 Admin 폴더로 복사한다.
app/Http/Controller/HomeController를 Admin 폴더로 복사한다.

이제 모델 Admin에 대한 컨트롤러는 아래와 같은 디렉토리 구조를 같는다.

  • app/Http/Controller/Admin/LoginController.php
  • app/Http/Controller/Admin/RegisterController.php
  • app/Http/Controller/Admin/ConfirmPasswordController.php
  • app/Http/Controller/Admin/ForgotPasswordController.php
  • app/Http/Controller/Admin/ResetPasswordController.php
  • app/Http/Controller/Admin/VerificationController.php
  • app/Http/Controller/Admin/HomeController.php

(2) 뷰 뼈대 만들기

resource/views 아래에 admin 폴더를 생성한다.
resource/views/auth 폴더를 복사해 resource/views/admin 아래에 붙여 넣는다.
resource/views/home.blade.php 복사해 resource/views/admin 아래에 붙여 넣는다.

(3) 구현하기

Admin 컨트롤러 각각의 파일을 열어 상단에 정의 되어있는 네임스페이스 정의를 변경해준다.

namespace App\Http\Controllers\Admin;

네임스페이스를 변경했으면 각 컨트롤러의 메소드 및 사용하고 있는 트레이트 메서드를 목적에 맞게 오버라이드 한다.

  • 로그인 기능 구현
#Admin/LoginController.php
#Auth 파사드 추가
use Illuminate\Support\Facades\Auth;

   protected $redirectTo = 'admin/home';

    public function __construct()
    {
        #미들웨어 변경, admin 파라미터 전달
        $this->middleware('admin.guest:admin')->except('logout');
    }

    #AuthenticatesUsers 트레이트 메서드 오버라이드
    public function showLoginForm()
    {
        return view('admin.auth.login');
    }

    #가드지정
    protected function guard()
    {
        return Auth::guard('admin');
    }
#admin/login.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.login') }}">
  • 회원가입
#Admin/RegisterController.php

#User 모델대신 Admin 모델을 사용
use App\Admin;
#Auth파사드 추가
use Illuminate\Support\Facades\Auth; 

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        $this->middleware('admin.guest:admin');
    }

    public function showRegistrationForm()
    {
        return view('admin.auth.register');
    }

    protected function validator(array $data)
    {
        #unique 속성에 파라미터 admins(테이블명) 전달
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:admins'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    protected function create(array $data)
    {
        return Admin::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }

    protected function guard()
    {
        return Auth::guard('admin');
    }
#admin/register.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.register') }}">
#Admin/HomeController.php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function __construct()
    {
        $this->middleware('admin.auth:admin');
    }

    public function index()
    {
        return view('admin.home');
    }
}
  • 비밀번호 확인

라라벨은 특정영역에 접근하기전에 비밀번호를 확인하도록 요청할 수 있는 기능이 있다.

#Admin/ConfirmPasswordController.php

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        #미들웨어에 파라미터를 전달해 가드를 지정
        $this->middleware('admin.auth:admin');
    }

    public function showConfirmForm()
    {
        return view('admin.auth.passwords.confirm');
    }
admin/auth/password/confirm.blade.php
<form method="POST" action="{{ route('admin.password.confirm') }}">

특정라우트를 보호하고 싶을때는 아래와 같이 미들웨어를 지정한다. admin.auth에는 admin 가드를 전달, password.cofirm에는 리다이렉트 라우트를 전달한다.

        Route::get('test', function(){
            return '이페이지가 보인다면 비밀번호 확인을 하신겁니다.';
        })->middleware(['admin.auth:admin', 'password.confirm:admin.password.confirm']);
  • 비밀번호 리셋

라라벨은 기본적으로 이메일로 인증주소를 받아 비밀번호를 초기화 시킨다. 이메일 설정이 되어있지 않으면 메일형식을 로그로 변경해주자.

# .env
MAIL_MAILER=log

비밀번호를 초기화하는 과정은 아래와 같다.

  1. admin/password/reset 페이지 접근
  2. 사용자 정보가 있으면 비밀번호 초기화페이지로 이동 없으면 로그인
  3. 이메일을 제출하면 해당 주소로 인증주소를 보낸다.
  4. 인증주소로 접속하여 비밀번호를 변경

이제 컨트롤러와 뷰를 수정하자

#admin/ForgotPasswordController.php

#Password 파사드 추가
use Illuminate\Support\Facades\Password;

    public function __construct()
    {
        $this->middleware('admin.auth:admin');
    }

    public function showLinkRequestForm()
    {
        return view('admin.auth.passwords.email');
    }

    public function broker()
    {
        return Password::broker('admin');
    }
#admin/auth/passwords/email.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.password.email') }}">

storage/logs/laravel.log 파일을 열어 인증주소를 확인해보면 localhost:8000/password/reset/token?email=your@email.com와 같은 형식으로 나타나는데 이 주소는 라라벨의 리셋주소이다. 따라서 Admin전용 리셋주소로 정보를 받아야 한다.

패스워드 재설정 이메일을 커스텀하려면 몇가지 작업이 필요하다.

  1. artisan 명령어를 이용해 notification 생성
  2. Admin모델에서 sendPasswordResetNotification 메서드 오버라이드

패스워드 재설정 참조링크

php artisan make:notification AdminPasswordResetMailToken
namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Lang;
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;

class AdminPasswordResetMailToken extends ResetPasswordNotification
{
    use Queueable;

    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }

        if (static::$createUrlCallback) {
            $url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
        } else {
            $url = url(config('app.url').route('admin.password.reset', [
                'token' => $this->token,
                'email' => $notifiable->getEmailForPasswordReset(),
            ], false));
        }

        return (new MailMessage)
            ->subject(Lang::get('Reset Password Notification'))
            ->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
            ->action(Lang::get('Reset Password'), $url)
            ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
            ->line(Lang::get('If you did not request a password reset, no further action is required.'));
    }
}

패스워드 재설정은 Illuminate\Auth\Notifications\ResetPassword의 클래스를 기본으로 사용하기 때문에 ResetPassword 을 상속받아 AdminPasswordResetMailToken를 작성해준다.

#Admin.php

use App\Notifications\AdminPasswordResetMailToken;

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new AdminPasswordResetMailToken($token));
    }

모델이 사용하는 Illuminate\Foundation\Auth\User 의 클래스에 지정되어 있는 CanResetPassword 트레이트의 메서드 sendPasswordResetNotification를 오버라이드한다.

이제 이메일주소를 입력하고 인증주소를 확인해보면 원하는 주소로 변경되었음을 확인 할 수 있다.

인증메일부분을 처리했으니 인증주소를 통해 넘어온 사용자에게 비밀번호를 받고 저장하는 부분을 처리해주자.

#Admin/ResetPasswordController.php

#Request, Auth, Password 파사드 추가
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;

    protected $redirectTo = '/admin/home';

    public function showResetForm(Request $request, $token = null)
    {
        return view('admin.auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }

    protected function guard()
    {
        return Auth::guard('admin');
    }

    public function broker()
    {
        return Password::broker('admin');
    }
#라우트 변경
<form method="POST" action="{{ route('admin.password.update') }}">
  • 이메일 인증하기

웹사이트를 사용하기전에 이메일을 인증해야하는 경우가 있다. 인증정보는 eamil_verified_at 필드에 저장된다.

기본인증기능에서는 라우트에 verify 옵션을 전달하면 사용할 수 있다.

Auth::routes(['verify' => true]);

커스텀한 인증에서는 비밀번호 초기화처럼 추가적으로 개발해야 한다.

#admin/VerificationController.php

use Illuminate\Http\Request;

    protected $redirectTo = '/admin/home';

    public function __construct()
    {
        $this->middleware('admin.auth:admin');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }

    public function show(Request $request)
    {
        return $request->user()->hasVerifiedEmail()
                        ? redirect($this->redirectPath())
                        : view('admin.auth.verify');
    }
#admin/auth/verify.blade.php
#라우트 변경
<form class="d-inline" method="POST" action="{{ route('admin.verification.resend') }}">

컨트롤러와 뷰쪽은 끝. 이제 인증메일을 발송하는 부분을 수정하자.

notification 클래스 생성

php artisan make:notification AdminVerifyEmail

Illuminate\Auth\Notifications\VerifyEmail 는 기본인증에 사용되는 클래스이다. Illuminate\Auth\Notifications\VerifyEmail 클래스를 상속받아 필요한 부분만 다시 정의해준다.

namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\URL;
use Illuminate\Auth\Notifications\VerifyEmail;

class AdminVerifyEmail extends VerifyEmail
{
    protected function verificationUrl($notifiable)
    {
        return URL::temporarySignedRoute(
            'admin.verification.verify',
            Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
            [
                'id' => $notifiable->getKey(),
                'hash' => sha1($notifiable->getEmailForVerification()),
            ]
        );
    }
}

생성한 AdminVerifyEmail 클래스를 Admin 모델에 적용해준다.

#Admin.php

use App\Notifications\AdminVerifyEmail;

    public function sendEmailVerificationNotification()
    {
        $this->notify(new AdminVerifyEmail);
    }