多欄位登入通用解決方案

GeorgeKing發表於2017-09-28

file

面臨的問題:

  • 登入欄位小於或等於2個的
  • 登入欄位大於或等於2個的

登入欄位不超過兩個

我在網上看到一種相對簡單解決方案,但是不能解決所有兩個欄位的驗證:

filter_var($request->input('username'), FILTER_VALIDATE_EMAIL) ? 'email' : 'name'

過濾請求中的表單內容,實現區分 username。弊端顯而易見,如果另一個不是 email 就抓瞎了……,下面是另一種通用的解決方案,在 LoginController 中重寫 login 方法:

public function login(Requests $request) {
    //假設欄位是 email
    if ($this->guard()->attempt(['email' =>$request->only('username'), 'password' => $request->only('password')]))) {
        return $this->sendLoginResponse($request);
    }

    //假設欄位是 mobile
    if ($this->guard()->attempt(['mobile' =>$request->only('username'), 'password' => $request->only('password')])) {
        return $this->sendLoginResponse($request);
    }

    //假設欄位是 username
    if ($this->guard()->attempt(['username' =>$request->only('username'), 'password' => $request->only('password')])) {
        return $this->sendLoginResponse($request);
    }

    return $this->sendFailedLoginResponse($request);
}

可以看到雖然能解決問題,但是顯然有悖於 Laravel 的優雅風格!你也可以參照medz1樓回覆的 方案。賣了這麼多關子,下面跟大家分享一下我的解決方案。

登入欄位大於或等於三個的(相對複雜一些)

file

為了方便理解我畫了個大致的流程,只畫了我認為重要的部分

  1. 首先需要自己實現一個 Illuminate\Contracts\Auth\UserProvider 的實現,具體可以參考 新增自定義使用者提供器 但是我喜歡偷懶,就直接繼承了 EloquentUserProvider,並重寫了 retrieveByCredentials 方法:

    public function retrieveByCredentials(array $credentials)
    {
    if (empty($credentials)) {
        return;
    }
    
    $query = $this->createModel()->newQuery();
    
    foreach ($credentials as $key => $value) {
        if (! Str::contains($key, 'password')) {
            $query->orWhere($key, $value);
        }
    }
    
    return $query->first();
    }

    注意: 框架源生的是 $query->where($key, $value);,多個欄位同時驗證,也就是說欄位之間是 and 的關係;將 $query->where($key, $value); 改為 $query->orWhere($key, $value);就可以了!

  2. 緊接著需要註冊自定義的 UserProvider:

    class AuthServiceProvider extends ServiceProvider
    {
            /**
             * 註冊任何應用認證/授權服務。
             *
             * @return void
             */
            public function boot()
            {
                    $this->registerPolicies();
    
                    Auth::provider('custom', function ($app, array $config) {
                            // 返回 Illuminate\Contracts\Auth\UserProvider 例項...
    
                            return new CustomUserProvider(new BcryptHasher(), config('auth.providers.custom.model'));
                    });
            }
    }
  3. 最後我們修改一下 auth.php 的配置就搞定了:

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'custom',
        ],
    
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],
    
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
    
         'custom' => [
             'driver' => 'custom',
             'model' => App\Models\User::class,
         ],
    ],
  4. 最後看一下 LoginController 的程式碼:

    public function login(LoginRequest $request)
    {
        $username = $request->get('username');
        $result = $this->guard()->attempt([
            'username' => $username,
            'email' => $username,
            'mobile' => $username,
            'password' => $request->get('password')]);

        if ($result) {
            return $this->sendLoginResponse($request);
        }

        $this->incrementLoginAttempts($request);
        return $this->sendFailedLoginResponse($request);
    }

現在哪怕你有在多個欄位都妥妥的……??

最後得得承認一下,在與medz討論的過程中有些上頭了 ,在這裡表示歉意。
後來我重新審視了一遍,覺得確實存在許多問題,於是繪製了認證流程圖,還請多多指點??
感謝medz的認真評論和回覆 :beers:


以下是 NicolaBonelli 給出的解決方案

我這裡按照他的思路只實現了login,其他細節還需自己根據實際需求進行修改,更多討論請看這裡20樓

  1. 首先需要在已有的users表上移除使用者憑證的欄位,如email、name等等,然後再建立另一個用於儲存使用者憑證的Certificate模型和表,大致結構如下圖所示:
    file
    users表定義如下:
    Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    //使用者暱稱
    $table->string('nickname')->nullable();
    $table->string('password');
    //使用者狀態
    $table->boolean('status')->default(true);
    $table->rememberToken();
    $table->timestamps();
    });

certificates表定義如下:

Schema::create('certificates', function (Blueprint $table) {
    $table->increments('id');
    //關聯使用者表
    $table->integer('user_id')->unsigned();
    //使用者驗證憑據
    $table->string('account')->unique();
    //憑據型別
    $table->string('type');
    $table->timestamps();
});
  1. 修改 config/auth.php 內容如下:
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'certificate',
    ],
],

'providers' => [
    'certificate' => [
        'driver' => 'eloquent',
        'model' => App\Certificate::class,
    ],
],
  1. 緊接著修改Certificate模型,讓他繼承Illuminate\Foundation\Auth\User這個類,並重寫 getAuthPassword方法:
/**
 * 定義模型關聯
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function user()
{
    return $this->belongsTo(User::class, 'user_id');
}

/**
 * 獲取使用者憑證所對應的密碼
 * @return string
 * @throws AuthenticationException
 */
public function getAuthPassword()
{
    if ($this->user()) {
        return $this->user->password;
    }
    throw new AuthenticationException();
}

到這裡多欄位登入功能算是實現了,但是登出時會因為certificates表中沒有定義remember_token欄位而導致丟擲異常。要解決這個問題,我們還要重寫logout方法,甚至重新實現一個自定義Guard……這裡就不做分析了,有興趣的可以自行Review Illuminate\Auth的原始碼!

最後感謝 NicolaBonelli 的分享 :beers:

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章