authentication

การเข้าระบบผ่านฟอร์ม มักใช้ Session ที่เก็บบนเซิร์บเวอร์ ส่วนการเข้าระบบผ่าน API มักใช้ Token เพราะไม่ผ่านเบราเซอร์โดยตรง ในบทนี้เราจะศึกษาการรักษาความปลอดภัยผ่านตัวช่วย ซึ่งเราไม่ต้องจัดการกับ Session หรือ Token โดยตรง

Start Kit

ชุดโปรแกรมตัวช่วยของ Laravel มีหลายตัว สำหรับตัวช่วยสำหรับการใช้งาน Session ที่ง่ายที่สุดคือ Laravel Breeze และสำหรับ API คืองานบริการของ Sanctum หรือ Passport แต่ความซับซ้อนของ Sanctum จะมีน้อยกว่า

สำหรับต้องการสร้างระบบที่ค่อยข้างสำเร็จรูป Laravel มี Start Kit ให้หลายตัว เช่น Jetstream, breeze, Fortify แต่สำหรับผู้เริ่มต้นแล้ว Laravel Breeze จะเป็นจุดเริ่มต้นที่ง่ายที่สุด

Laravel Breeze

Laravel Breeze มีคุณสมบัติในการยืนยันตัวตน ที่รวมการใช้ฟอร์มเข้าระบบ การลงทะเบียน การสร้างรหัสเข้าระบบใหม่ การยืนยันผ่านอีเมล์ และการยืนยันรหัสผ่าน

การติดตั้ง

ก่อนอื่นต้องสร้างฐานข้อมูล MySQL ในชื่อ Laravel และสร้าง WebApp ในชื่อ example-app (จะต้องสร้าง WebApp ขึ้นใหม่ ถ้าใช้ WebApp ที่แก้ไขมาก่อนอาจมีปัญหาได้) แล้วต่อด้วยย้ายฐานข้อมูลด้วย migrate

CLI 1.


D:\>composer create-project laravel/laravel example-app
D:\>cd example-app
D:\example-app>php artisan migrate

ถึงขั้นตอนนี้ เท่ากับว่าตารางข้อมูลได้สร้างไว้ใน Laravel แล้ว ต่อไปก็ใส่ชุด Breeze ลงใน example-app

CLI 2.


D:\example-app>composer require laravel/breeze
D:\example-app>php artisan breeze:install
D:\example-app> php artisan serve

ในขั้นตอน >php artisan breeze:install จะมีการถามว่าให้เลือก UI(Stack) ในรูปแบบใด ให้เลือก blade และคำถามอื่น ๆ ให้ตอบ No ให้หมด ยกเว้นจะเลือกแบบที่ต้องการ


 Which stack would you like to install?
  blade .......................................................... 0
  react .......................................................... 1
  vue ............................................................ 2
  api ............................................................ 3

ให้พิมพ์ 0 เพื่อเลือก blade

ผลที่ได้ คือทุกอย่างมาครบ คือ หน้า Login, Register และ Reset Password

รูป 1 หน้าเริ่มต้น (Welcome)

รูป 2 หน้าเข้าระบบ (Login)

สำหรับ ชุดเริ่มต้นของ Breeze หรือ Jetstream จะหยุดการให้เข้าระบุเป็นเวลา หนึ่งนาที ถ้ามีการพยายามเข้าระบบหลายครั้งแล้วทำเข้าระบบไม่ได้

สร้างระบบป้องกันด้วยตนเอง

Auth::attempt( )

หากต้องการสร้างระบบ Login ด้วยตนเอง ก็สามารถทำได้ถึงแม้จะยังคงใช้ฟังก์ชันสำเร็จหลายตัวอยู่ก็ตาม ดังใช้การฟังก์ชัน Auth::attempt($credentials) ที่ทำหน้าที่ตรวจสอบ $credentials ที่มี email และ password ที่ใช้เป็นรหัสเข้าระบบ ตามฐานข้อมูลที่ตรงกับตาราง users ที่ใช้ก่อนหน้านี้ (ที่ได้จากการ migrate ตารางที่มีมาให้) โดยไม่ต้องเขียนโปรแกรมตรวจสอบด้วยตนเอง ดังเขียนเป็น ฟังก์ชัน login ไว้ใน LoginController.php

Code 1. Http/Controllers/LoginController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
    * Handle an authentication attempt.
    *
    * @param    \Illuminate\Http\Request  $request
    * @return  \Illuminate\Http\Response
    */
    public function login(Request $request){
        //$email = $request->email;
        //$password = $request->password;

        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);
        
        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();
            return redirect()->intended('/');
        }
    
        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ]);
    }
}

โดยฟังก์ชัน login( ) มีตัวแปรเข้าเป็น Request ซึ่งจะเป็น ค่าของ $request->email และ $request->password เป็นสำคัญที่ใช้ในการเข้าระบบ ดังนั้นฟอร์มที่ใช้เข้าระบบจะต้อง มีชื่อ <input> เป็น email และ password ที่ตรงกันด้วย และส่งคำขอบริการมายังคอนโทรลเลอร์นี้

Code 2. resources/views/login.blade.php

<div id='login'>
    <h2>Login</h2>
    <form method='POST' action='login'>
    @csrf
    <div class="mb-3">
        <label class="form-label">Email address</label>
        <input type="email" name='email' 
               class="form-control" placeholder="name@example.com">
        </div>
        <div class="mb-3">
            <label class="form-label">Password</label>
            <input type="password" name='password' 
                   class="form-control" placeholder="password">
        </div>
        <div class="mb-3"> 
            <button type='submit'
                class="btn btn-primary"  
                style="float:right; margin-righ:5px">Login</button>
        </div>
    </form>
</div>

ในฟังก์ชัน Auth:attempt($credentials) ใช้การเข้ารหัส ซึ่งเราตรวจสอบได้ โดยที่ ตัวแปรแรกมาจาก ข้อมูลที่ส่งมาจากฟอร์ม และตัวแปรที่สอง รหัสที่อ่านจากฐานข้อมูล ดังดูได้จากตัวอย่างต่อไปนี้


//use Hash;
if (Hash::check($credentials['password'], $hashedPassword)) {
    return "Match";
 }
else{
    return "No-match";
 }

ถ้าต้องการเข้ารหัสก็ใช้ฟังก์ชัน Hash::make('plain-text') ซึ่งทำการใช้คำสั่งนี้แต่ครั้งจะได้ รหัสลับที่ไม่ตรงกัน ควรใช้แบบ Hash::check( ) เพื่อตรวจสอบความตรงกันของรหัสลับดีกว่า

$request->validate( )

สำหรับฟังก์ชัน $request->validate( ) ใช้สำหรับตรวจสอบเงื่อนไขว่าตรงตามกฎหรือไม่ กฎก็มีได้มากมาย ในตัวอย่างของฟังก์ชัน Login( ) นี้ ใช้กฎ ['required', 'email'] ซึ่งหมายถึงต้องมีมา และต้องเป็นรูปแบบอีเมล หากไม่เป็นตามกฎ จะทำให้กลับไปยังหน้าเว็บก่อนหน้านี้โดยอัตโนมัติ โดยไม่จำเป็นต้องเขียนคำสั่ง redirect แต่อย่างใด

รูป 3 กฎที่มีสำหรับฟังก์ชัน Validate

https://laravel.com/docs/10.x/validation#available-validation-rules

Auth::user( )

ถึงแม้ในตัวอย่างที่ผ่านมาจะไม่ได้การใช้อ่านข้อมูลที่ผ่าน Authentication แล้วก็ตาม แต่ถ้าเราต้องการที่จะอ่านข้อมูลของผู้ที่่ผ่านการ Authentication แล้วสามารถใช้ฟังก์ชัน Auth:: user( ) ได้ เช่น


$user = Auth::user( )
//$request->user( ) //results are the same as above 
//Auth:: id( ) // get id $user that has authenticated
echo $user;

/* output
{
  "id": 2,
  "name": "Theerapol Limsatta",
  "email": "theerapol.lim@gmail.com",
  "email_verified_at": null,
  "created_at": "2023-03-01T03:37:42.000000Z",
  "updated_at": "2023-03-01T03:37:42.000000Z"
}
*/

Auth:: check( )

คำสั่ง Auth:: check( ) จะคืนค่าจริง ถ้าผ่านการตรวจสอบสิทธิ์แล้ว แต่อย่างไรก็ตาม เว็บเบราเซอร์จะยังคงจำข้อมูลเดิมไว้หาตรวจสอบสิทธิ์ผ่านแล้ว จึงควรใช้วิธีอื่นจะดีกว่า ในการตรวจสอบวิธีอื่นดีกว่า

การป้องกันการเข้าใช้สิทธิ์ในเส้นทางอื่น

เรามีระบบ Login แล้ว แต่จะให้ Login ในทุก ๆ หน้าเป็นเรื่องไม่ดีแน่ เราควรมีการป้องกันหน้าเว็บอื่นโดยไม่ต้องตรวจสอบสิทธิอีกครั้ง ใน Laravel มีตัวกลางสื่อสาร (middleware) ที่ทำงานให้อัตโนมัติ โดยไม่ต้องสร้างขึ้นมาใหม่ทั้งหมด เช่น เราต้องการป้องกัน ในเส้นทาง /profile ที่ต้องผ่านการตรวจสอบสิทธิ์ก่อนเข้าเส้นทางนี้

Code 3. routes/web.php

Route::get('/profile', function () {
    return view('profile');
})->middleware('auth.basic');

ด้วยการเติม middleware('auth.basic') หลังเส้นทาง หาว่ามีผู้ใช้เส้นทางนี้ ยังไม่ผ่านการตรวจสอบการใช้สิทธิก็จะมีหน้าต่างการกรอกรหัสก่อน

รูป 4 หน้าต่างตรวจสอบสิทธิ์

อีกวิธีที่จะป้องกันหายังไม่ได้รับรองสิทธิ์การใช้ ใช้ แค่ 'auth' แทน 'auth.basic'


middleware('auth')

และจะโยนกลับไปหน้า login ซึ่งเขียนกำกับไว้แล้วที่ ไฟล์ app/Http/Middleware/Authenticate.php หากเราจะโยนไปหน้าอื่นก็ให้แก้ไขชื่อ เส้นทางใหม่ได้

Code 4. Http/Middleware/Authenticate.php

use Illuminate\Http\Request;
/**
 * Get the path the user should be redirected to.
 */
protected function redirectTo(Request $request): string
{
    return route('login'); // name of rout
}

ดังนั้นแล้วจะต้องตั้งชื่อให้กับเส้นทางด้วย ในชื่อ login

Code 5. routes/web.php

Route::get('/login', function () {
     return view('login');
})->name('login'); // add name of rout

back()->withErrors

ในตัวอย่างมีการส่งข้อมูลกลับไปยังเว็บก่อนหน้าที่จะเข้า (login) ได้มีการเพิ่ม ชื่อ (email) และ ข้อความ (message)


return back()->withErrors([
     'email' => 'The provided credentials do not match our records.',
]);

ค่าที่ไว้อ้างอิงความผิดพลาด เรานำค่าอ้างอิงนี้ไปใช้ได้ที่หน้า Login เพื่อแจ้งผลความผิดพลาด เช่น เราอาจในส่วนใดส่วนหนึ่งของหน้าเว็บ login.blade.php


@error('email')
    <p>{{$message}}</p>
@enderror

แต่ถ้าเราเพิ่ม olnInput( ) เข้าไปอีก เพื่อนำไปใช้ในค่าใน <input>


return back()->withErrors([
    'email' => 'The provided credentials do not match our records.',
])->onlyInput('email');

เราจะต้องเติมค่าเก่าที่เคยใช้ส่งไปในการเข้าระบบ ด้วยฟังก์ชัน old( )


<input type="email" name='email' value="{{ old('email') }}" 
    class="form-control" placeholder="name@example.com">

เพิ่มเงื่อนไขการเข้าระบบ

ถ้าหากว่าต้องการที่จะเพิ่มเงื่อนไขเพิ่มเติมในการเข้าระบบ เช่นว่า เพิ่มฟิลด์ active เข้าอีกอีกฟิลด์ (ในฐานข้อมูลต้องเพิ่มฟิลด์นี้ด้วย แต่ใน Model อาจไม่แก้ไขก็ได้) โดยการแก้ไขฟังก์ชัน Auth::attemp( ) ข้อมูลเข้าเป็นอาร์เรย์แบบมีคีย์ (associative array)

Code 6. Http/Controllers/LoginController.php

$email = $request->email;
$password = $request->password;

if (Auth::attempt([
    'email' => $email, 'password' => $password, 'active' => 1
])) {
    // Authentication was successful...
}

logout

เมื่อต้องออกจากระบบ ใช้เพียงฟังก์ชัน Auth::logout() ล้าง session ที่มี และเปลี่ยนเส้นทางใหม่

Code 7. Http/Controllers/LoginController.php

use Illuminate\Http\RedirectResponse;
public function logout(Request $request): RedirectResponse 
{
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return redirect('/');
}

สำหรับการทำส่งคำสั่ง Logout ซึ่งสมมุติอยู่ที่หน้า Profile ซึ่งการตรวจสอบก่อนว่า มีเส้นทางชื่อ login หรือไม่ ถ้ามี ใช้ @auth .. @endauth (Authentication Directives) เพื่อตรวจว่ามีการใช้ระบบตรวจสอบหรือไม่ หรือมีการเข้าระบบหรือไม่ ถ้ามี ก็ให้แสดง <a> ที่ไปยัง Logout

Code 8. resources/views/profile.blade.php

<h2>Profile</h2>
@if (Route::has('login'))
    <div>
    @auth
     <a href="{{ url('/') }}">Welcome</a> | 
     <a href="{{ route('logout') }}" >Log out</a>   
     @endauth
    </div>
@endif