Middleware

สื่อตัวกลาง (Middleware)

สื่อตัวกลาง หมายถึง กลไกการทำงานในการตรวจสอบหรือกรองการทำงานของคำขอ(request) HTTP คำขอ HTTP เช่น คำขอในด้าน input, cookies, file ที่ส่งมาพร้อมกับคำของ ยกตัวอย่างเช่น การตรวจสอบคำขอถึงสิทธิ์ในการเข้าระบบเว็บ ถ้าคำขอที่ส่งข้อมูลระบุตัวตนถูกต้อง ก็จะถือว่าเข้าระบบได้ ในการทำงานนี้สื่อตัวกลางจึงทำหน้าที่นี้ เป็นเหมือนตัวขั้นกลางก่อนการทำงานอื่นต่อไป

สร้างสื่อตัวกลาง

การสร้างใช้ CLI สมมุติให้ชื่อ EnsureTokenIsValid เพื่อจำลองการอ่านรหัสลับ (Token) ที่ส่งมาตรวจสอบก่อนที่จะให้สามารถผ่านไปหน้าเว็บต่อไปได้ (การสร้างพิมพ์สร้างสื่อตัวกลางนี้ make:middleware ต้องพิมพ์ติดกัน)

CLI 1.


>php artisan make:middleware EnsureTokenIsValid
        

เมื่อสร้างจาก CLI ให้แก้ไขในส่วนฟังก์ชัน handle( ) โดยการตรวจสอบว่า รหัสลับต้องเป็น my-secret-token จึงจะไปยังหน้าต่อไปได้ ถ้ารหัสไม่ผ่านก็ให้กลับไปยังหน้า home หน้าเว็บ home ให้สร้างไฟล์นี้ชื่อ home.blade.php รอรับการทำงานไว้ด้วย

Code 1. app/Http/Middleware/EnsureTokenisValid.php

<?php
namespace App\Http\Middleware;

use \Illuminate\Http\Request;
use Closure;

class EnsureTokenIsValid
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->input('token') !== 'my-secret-token') {
            return redirect('home');
        }
        return $next($request);
    }
}
        

ลงทะเบียนการใช้สื่อตัวกลาง

เพื่อการใช้งานสื่อตัวกลาง ให้ลงทะเบียนไว้ที่ไฟล์ Kernel.php ไฟล์นี้จะทำให้สื่อตัวกลางที่ลงทะเบียนใช้งานได้ทั้งเว็บแอปฯ โดยการเขียนชื่อของสื่อตัวกลางในส่วนอาร์เรย์ของ $routeMiddleware ในชื่อที่ใดก็ได้ ในที่นี้ใช้ชื่อว่า ‘ensuretoken’ โดยผูกไว้กับไฟล์สื่อตัวกลางที่ได้สร้างไว้

Code 2. app/Http/Kernel.php

protected $routeMiddleware = [
    // สื่อตัวกลางอื่น ๆ
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    'ensuretoken' => \App\Http\Middleware\EnsureTokenIsValid::class,
];
        

สร้างเส้นทางรองรับสื่อตัวกลาง

จากสื่อตัวกลางที่ใช้ตรวจสอบข้อมูลเข้าจากคำขอของ HTTP โดยสมมุติว่า มีเส้นทางไปยังหน้าเว็บ welcome ต้องผ่านสื่อกลางก่อนให้ใช้ฟังก์ชัน middleware(‘ensuretoken’) ต่อท้ายฟังก์ชัน get()

Code 3. routes/web.php

Route::get('/', function (Request $request) {
    return view('welcome');
})->middleware('ensuretoken');
        

ฟังก์ชัน get( ) เป็นการขอบริการของ HTTP ผ่าน URL: / ซึ่งถ้าเราทดลองใช้

การสร้างเส้นทางรองรับสื่อตัวกลางที่มีหลายสื่อตัวกลาง สามารถใส่พร้อมกันหลายตัวได้ในรูปอาร์เรย์ เช่น


Route::get('/', function(){
})->middleware('ensuretoken', 'auth');
        

หรือจะใส่ชื่อคลาสแทนชื่อที่ลงทะเบียนก็ได้ และด้วยวิธีการนี้ก็ไม่จำเป็นต้องลงทะเบียนในไฟล์ Kernel.php เช่น


Route::get('/'', function(){
})->middleware(EnsureTokenIsValid::class);
        

สร้างสื่อตัวกลางรองรับ CORS

CORS ย่อมาจา Cross Origin Resource Sharing ประกอบด้วย โดเมน และเลขที่พอร์ทสำหรับสื่อสาร เช่น http://127.0.0.1:8000 การสื่อสารกันได้ภายใต้ CORS จะต้อง จำกัดภายใต้โดเมน และเลขที่พอร์ท นี้เท่านั้น เช่น ถ้าใช้ http://127.0.0.1:4200 ก็จะถือว่า ผิดนโยบาย CORS ที่ทำให้สื่อสารกันไม่ได้ และยิ่ง เป็นโดเมนอื่น ก็ยิ่งผิดนโยบายไปมาก

ในการพัฒนาเว็บแอปพลิเคชัน บางครั้งก็ จำเป็นต้อง ทดลองการทำงานร่วมกัน ที่ฝืนนโยบายของ CORS บ้าง การฝืนนโยบาย ทำได้ทั้งในฝั่งไคลเอ็นท์ และเซิร์บเวอร์ การแก้ที่เซิร์บเวอร์เป็นวิธีการทำได้ง่าย และแน่นอนที่สุด ดังนั้นเรามาสร้างการฝืนนโยบายนี้ โดยสร้างเป็น สื่อตัวกลาง Cors

CLI 4.


> php artisan make:middleware Cors
        

สร้าง header ที่รองรับการสื่อสารข้ามโดเมนและพอร์ท โดยให้ยอมรับ localhost:4200ซึ่งมาจากเว็บที่พัฒนาจาก Angularดังแก้ไขคลาส Cors ดังนี้

Code 4. app/Http/Middleware/Cors.php

class Cors{
    public function handle(Request $request, Closure $next){
        return $next($request)
            ->header('Access-Control-Allow-Origin', '*')
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        }
}
        

และลงทะเบียนกับ Kernel ในระดับ Global คือใช้ได้กับทุกเส้นทาง

Code 5. app/Http/Kernel.php

protected $middleware = [
    // สื่อตัวกลางอื่น ๆ ของระดับ Global
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\Cors::class,  //ตัวที่เพิ่มไปใหม่
];
        

สร้างเส้นทาง products ทดสอบ การอ่านให้อ่านค่าออกเป็น JSON

Code 6. routes/web.php

Route::get('/products', function(){
    $pro1 = array('id'=>1, 'name'=>'RAM');
    $pro2 = array('id'=>2, 'name'=>'Harddisk');
    return array($pro1, $pro2);
});
        

ทำสอบการอ่านข้าม Port โดยใช้ Port: 80 ของเซิร์บเวอร์ของ XAMPP เก็บไฟล์งานที่โฟลเดอร์ test_cors เมื่อทดสอบจะสามารถอ่านข้อมูลได้ แต่ถ้าทดลองเปลี่ยนโดยการเอาสื่อตัวกลาง Cors ออกจาก Kernel.php จะพบว่าไม่สามารถเข้าถึงข้อมูลได้

Code 6. xampp/htdocs/test_cors/index.html

<!DOCTYPE html>
<html lang="en">
<body>
<h2>Title</h2>
<ul></ul>
<script 
src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js">
</script>      
<script>	
$.ajax({
	method:"GET",
	url:"http://127.0.0.1:8000/products",
	dataType:"json",			
    	success:function(data){ 
      	    data.forEach(i=>{
	    	    str = i.id + ":"+ i.name;
        	    $('<li>').text(str).appendTo('ul');
      	    });		
    	},
    	error:function(){
      	    alert("Error");
    	}      
});
</script>
</body>
        

สื่อตัวกลางแบบกลุ่ม

จากที่ผ่านมา การใช้งานสื่อตัวกลางต้องเขียนต่อท้ายการสร้างเส้นทาง แต่ถ้าต้องการให้ใช้ได้กับเส้นทางอื่นพร้อมกัน โดยไม่ต้องเขียนแต่ละเส้นทาง วิธีการคือต้องสร้างสื่อตัวกลางแบบกลุ่ม

การสร้างกลุ่มของสื่อตัวกลาง ใช้การลงทะเบียนในไฟล์ Kernel.php ในตัวแปร $middlewareGroups ซึ่งมี web และ api ได้มีลงทะเบียนไว้แล้ว

Code 8. app/Http/Kernel.php

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];
        

เมื่อลงทะเบียนไว้แล้ว ก็นำไปใช้กับระบบเส้นทางได้ ซึ่งใช้ในลักษณะเดียวกับสื่อตัวกลางเดี่ยว (ไม่เป็นกลุ่ม) และใช้ในลักษณะกลุ่มโดยตรงก็ได้ ดัง 2 ตัวอย่างต่อไปนี้


Route::get('/', function () {
    	     //
})->middleware('web');

Route::middleware(['web'])->group(function () {
    	    //
});
        

สื่อตัวกลางทำงานตามลำดับ

ในบางครั้งเราอาจต้องการให้สื่อตัวกลางทำงานตามลำดับ กรณีเช่นนี้ ให้ลงทะเบียนผ่านตัวแปรชื่อ $middlewarePriority ในไฟล์ Kernel.php ตัวแปรนี้ยังได้กำหนดมาตั้งแต่ต้น ดังนั้นหากเราต้องการทำงานตามลำดับ ก็ต้องสร้างขึ้นเอง

Code 9. app/Http/Kernel.php

protected $middlewarePriority = [
    \Illuminate\Cookie\Middleware\EncryptCookies::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
    \Illuminate\Routing\Middleware\ThrottleRequests::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];
        

ส่งตัวแปรไปกับสื่อตัวกลาง

การรับตัวแปรมากับสื่อตัวกลาง จะเป็นตัวแปรที่ สาม ต่อกับตัวแปร $next ของฟังก์ชัน handle เช่น ใช้ตัวแปรในชื่อ $role ในตัวอย่างต่อไปนี้ สมมุติเหตุการณ์เป็นการตรวจสอบตัวแปร $role ว่ามีค่าเป็น “manager” หรือไม่ ส่วนสองตัวแปรแรกค่าเป็นค่าตัวแปรที่ใส่มาให้ตั้งแต่สร้างฟังก์ชัน handle

Code 10. app/Http/Middleware/EnsureUserHasRole.php

<?php
namespace App\Http\Middleware;

use Closure;
class EnsureUserHasRole
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role)  {
        if ($role !== 'manager') {
            return redirect('/');
        }
        echo $role;
        return $next($request);
    }
}
        

และเช่นเคยลงทะเบียนสื่อตัวกลาง

Code 11. app/Http/Kernel.php

protected $routeMiddleware = [
    // สื่อตัวกลางอื่น ๆ
    'role' => \App\Http\Middleware\EnsureUserHasRole::class,
];
        

สำหรับการส่งตัวแปรไปกับเส้นทาง ใช้เครื่องหมาย : นำหน้าตัวแปรที่จะไปกับสื่อตัวกลาง ตามตัวอย่างต่อไปนี้

Code 12. routes/web.php

Route::get('/admin', function () {
    return view('admin');
})->middleware('role:manager');
        

สำหรับหน้า view: admin ก็ให้สร้างรองรับไว้ ในชื่อไฟล์ admin.blade.php และทำการทดสอบตาม URI : /admin

หยุดการทำงานสื่อตัวกลาง

สื่อตัวกลางจะหยุดทำงานอัตโนมัติเมื่อ มีการตอบสนอง (response) กลับมายังเบราเซอร์ แต่เราสามารถจัดการในขั้นตอนที่สื่อตัวกลางจะหยุดทำงาน โดยการใช้ฟังก์ชัน terminate($request, $response) ซึ่งฟังก์ชันนี้รองรับตัวแปรสองตัว

Code 13. app/Http/Middleware/TerminateMiddlewar.php

<?php
namespace App\Http\Middleware;

use Closure;
use \Illuminate\Http\Request;
use  \Illuminate\Http\Response;

class TerminateMiddlewar
{
    public function handle($request, Closure $next)
    {
        echo "Handle method worked.";
        return $next($request);
    }
    public function terminate($request, $response)
    {
        echo "Terminate method worked.";
    }
}
        

ต่อมาทำการลงทะเบียนกับไฟล์ Kernel.php ในชื่อ “terminate”

Code 14. app/Http/Kernel.php

protected $routeMiddleware = [
    // สื่อตัวกลางอื่น ๆ
    'role' => \App\Http\Middleware\EnsureUserHasRole::class,
    'terminate' => \App\Http\Middleware\TerminateMiddlewar::class,
];
        

ทดสอบกับเส้นทางเดิม แต่เพิ่มสื่อตัวกลางไปกับ role ในรูปอาร์เรย์ ทำให้เส้นทาง /admin ทำงานสองสื่อตัวกลาง ซึ่งจะได้ผลการพิมพ์ค่า "Terminate method worked" เป็นลำดับสุดท้าย

Code 15. routes/web.php

Route::get('/admin', function () {
    return view('admin');
})->middleware(['role:manager', 'terminate']);