Middleware & Session處理

Middleware & Session處理

Middleware & Session處理

Middleware & Session處理, 相信很多人的專案都有類似功能:我們希望使用者在使用某個功能或是頁面時是有時間限制的,若超過時間都沒有提他動作時再訪問時就會被踢出,或是需要重新驗證。一般筆者再處理的時候都是在controller內處理,今天發現同事使用Middleware 去處理這方面的問題,好像也是個不錯的處理方式,做個筆記來記錄一下

Middleware & Session處理

這邊用個實例來模擬比較有記憶點。

有一個提供查詢轉帳明細的頁面,但是帳務明細是相當敏感的資料,所以我們需要輸入密碼才可以進入轉帳。我們把名稱都定義為Account ,每個 Account在看帳務明細時需要輸入另外的密碼code ,在進入到帳戶明細中後,若五分鐘沒有動靜的話,再執行任務時需再輸入一次code

需先新增帳戶密碼,database\migrations 新增TIMESTAMP_create_accounts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateAccountsTable extends Migration {

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('accounts', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('account')->comment('帳戶名稱');
            $table->string('code')->comment('帳戶密碼');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('accounts');
    }
}

並跑

php artisan migrate後去資料庫內生資料出來。我是比較喜歡用tinker 搭配 factory的作法處理。

主要的程式碼分成兩個部分:Account.php(Middleware) 及 Route和AccountController,我們先來定義路由。

App\routes\web

//...
Route::group(['prefix' => 'accounts', 'as' => 'account', 'middleware' =>  'account'], function () {

  Route::get('/code', 'AccountLoginController@showLoginForm')->name('.loginform');//登入頁
  Route::post('/code', 'AccountLoginController@login')->name('.login');//驗證密碼
  Route::get('/', 'AccountController@index')->name('.index');//資料主頁,我們這邊不實作主頁內容,主要著重在middleware
//...

接下來是AccountLoginController:我們這邊先定義Middleware的幾個功能 (TDD的概念?)

  • isVerified():確認是否已驗證,若已驗證則轉跳到主頁面。
  • redirect():回到主頁面。
  • verify($code):驗證登入內容。應該要比對Account密碼。
<?php

namespace App\Http\Controllers\Account;

use App\Http\Controllers\Controller;
use App\Http\Middleware\Account;

class AccountLoginController extends Controller
{
    private $account;

    public function __construct(Account $account)
    {
        $this->account = $account;
    }

    public function showLoginForm()
    {
        if ($this->account->isVerified()) {
            return $this->account->redirect();
        }
        return view('account/login');
    } 

    public function login(Request $request)
    {
        if ($this->account->verify($request->code)) {
            return $this->account->redirect();
        }

        return back()->withErrors(['msg' => '驗證失敗']);
    }
}

最後是最重要的Middleware。

Middleware

App\Http\Middleware 內新增一個Account.php

//Account.php
<?php

namespace App\Http\Middleware;

use Closure;

class Account
{
    const SESSION = 'code';//存放在Session的Key值
    const PREVIOUS = 'previous';//處理無限跳轉問題,可參考inExceptArray()
    const REDIRECT_TO = 'account.index';//轉跳的主頁
    const EXPIRED_MINS = '+ 5min'; //輸入Session過期時間
    
    protected $except = [//當使用 $request->is()的時候帶入,可確認是否來自accounts/code,避免無限重導
        'accounts/code'
    ];

    //在每次進入middleware時,若有驗證過則表示已經登入過,會自動刷新驗證時間並通過,若是輸入登入頁面進來的,可以通過middleware,其他在middleware底下的頁面,即先儲存路由,若登入成功則取出路由並跳轉到該路由。
    public function handle($request, Closure $next)
    {
        if ($this->isVerified()) {
            $this->setVerified();
            return $next($request);
        } elseif ($this->inExceptArray($request)) {
            return $next($request);
        }

        session([self::PREVIOUS => \Route::currentRouteName()]);
        return redirect(route('account.loginform'));
    }
    //這邊利用Auth的方式去取得code,且用Hash的方式比對,所以code 在存入table 前要先經過Hash!
    public function verify($code)
    {
        if (\Hash::check($code, \Auth::user()->code)) {
            $this->setVerified();
            return true;
        }
    }
    //儲存驗證的expire time 到session
    public function setVerified($min = null)
    {
        if (!$min) {
            $min = self::EXPIRED_MINS;
        }
        session([self::SESSION => strtotime($min)]);
    }
    //從session取值,判斷是否有驗證
    public function isVerified()
    {
        $timestamp = session(self::SESSION);
        return $timestamp and $timestamp >= time();
    }
    //轉跳功能
public function redirect()
    {
        $route = session(self::PREVIOUS);
        if (!$route) {
            $route = self::REDIRECT_TO;
        }
        return redirect(route($route));
    }
    //由Request確認當前路由是否為登入畫面,若是則回傳True(到Handle時通過),若否則回傳False(到 Handle時儲存路由,並跳轉到登入畫面,登入成功後跳轉到該路由)
    protected function inExceptArray($request)
    {
        foreach ($this->except as $except) {
            if ($request->is($except)) {//判斷$request是否有該path
                return true;
            }
        }
        return false;
    }
}

關於 Except的重導問題可以參考Illuminate\Foundation\Http\Middleware\VerifyCsrfToken`應該會比較好理解。

另外記得要註冊 middleware 否則routes內無法使用

App\Http\Kernel.php

//...
protected $routeMiddleware = [
//...
        'account' => \App\Http\Middleware\Account::class,
];

View的部分相對簡單:

    <div id="wrapper">
        <div class="container">
            <div class="row" style="margin-top:20px">
                <div class="col-xs-12 col-sm-8 col-md-5 col-sm-offset-2 col-md-offset-3">
                    <form id="loginForm" method="POST" action="{{ route('account.login') }}">
                        {{ csrf_field() }}
                        <h4 class="text-center"><i class="fa fa-user" aria-hidden="true"></i> 帳戶驗證</h4>
                        <hr class="colorgraph">
                        <div class="form-group">
                            <input type="password" name="code" class="form-control input-xs" placeholder="帳戶驗證碼" data-parsley-required >
                            <p class="help-block">驗證後有效時間為 5 分鐘</p>
                        </div>
                        @if ($errors->has('msg'))
                        <div class="alert alert-danger" role="alert">
                          {{ $errors->first('msg') }}
                        </div>
                        @endif
                        <hr class="colorgraph">
                        <div class="row">
                            <div class="col-xs-12 col-sm-12 col-md-12">
                                <button type="submit" class="btn btn-primary btn-block">驗 證</button>
                            </div>
                        </div>
                    </form>

                </div>
                <!-- /.col-xs-12 col-sm-8 col-md-5 col-sm-offset-2 col-md-offset-3 -->
            </div>
            <!-- /.row -->
        </div>
        <!-- /.container -->
    </div>
    <!-- /#wrapper -->

這樣應該就算簡單完成用 Middleware做Session的驗證了。