基於 Laravel Passport API 的多使用者多欄位認證系統(三):多欄位登入

黃冬瓜發表於2018-01-05

3. 多欄位登入

傳統意義的多欄位登入,是通過 userid 或 username 或 email 這類任意一個 unique 欄位+密碼驗證。例如京東:

file

通過 Laravel Passport 自帶的 findForPassport ,做一點小小的修改即可完全滿足:

public function findForPassport($username) {
  return $this->orWhere('email', $username)->orWhere('phone', $username)->first();
 }

對於一些特殊的固定欄位,例如使用者是否啟用,也可以採用以下方式做驗證,參見 Issue

public function findForPassport($username)
    {
        $user = $this->where('email', $username)->first();

        if($user !== null && $user->status == 0) {
            throw new OAuthServerException('User account is not activated', 6, 'account_inactive', 401);
        }
        return $user;
    }

但是,如果想同時滿足多個欄位的驗證,例如上一章的 Student ,必須同時滿足 school_id 和 student_no 的條件

Student::create([
   'school_id'  => '10001',
   'student_no' => '17000003001',
   'name'       => 'Abbey',
   'password'   => bcrypt('st001')
        ]);

因為 findForPassport 預設只傳遞了一個 username 的欄位,因此還是需要自己擴充套件。並且我也想可以自定義欄位名,不一定必須使用username。

以下部分程式碼參考了@RyanLaravel Passport API 認證使用小結,特別表示感謝。

上一章已經定位到了原始碼的 vendor/league/oauth2-server/src/Grant/PasswordGrant.php,有一個validateUserd的函式呼叫了getUserEntityByUserCredentials

protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
    {
        $username = $this->getRequestParameter('username', $request);
        if (is_null($username)) {
            throw OAuthServerException::invalidRequest('username');
        }

$password = $this->getRequestParameter('password', $request);
        if (is_null($password)) {
            throw OAuthServerException::invalidRequest('password');
        }

$user = $this->userRepository->getUserEntityByUserCredentials(
            $username,
            $password,
            $this->getIdentifier(),
            $client
        );
        if ($user instanceof UserEntityInterface === false) {
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidCredentials();
        }

return $user;
    }

因此,需要先把PasswordGrant擴充套件出來。

3.1 在App\Auth新建PasswordGrant.php,程式碼如下:

<?php

namespace App\Auth;

use League\OAuth2\Server\Grant\PasswordGrant as BasePasswordGrant;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestEvent;
use Psr\Http\Message\ServerRequestInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;

class PasswordGrant extends BasePasswordGrant
{

    /**
     * @param ServerRequestInterface $request
     * @param ClientEntityInterface  $client
     *
     * @throws OAuthServerException
     *
     * @return UserEntityInterface
     */
    protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
    {
        $username = $this->getRequestParameter('username', $request);
        $provider = $this->getRequestParameter('provider', $request);
        if(is_null($provider) && is_null($username)){
            throw OAuthServerException::invalidRequest('username');
        }
        $password = $this->getRequestParameter('password', $request);
        if (is_null($password)) {
            throw OAuthServerException::invalidRequest('password');
        }
        $user = $this->userRepository->getUserEntityByUserCredentials(
            $username,
            $password,
            $this->getIdentifier(),
            $client
        );

        if ($user instanceof UserEntityInterface === false) {
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

            throw OAuthServerException::invalidCredentials();
        }

        return $user;
    }

}

主要改動邏輯為:
如果傳遞的引數有provider,則為多使用者登入,不用強制驗證username了。
當然這裡也可以加更多邏輯,按照實際需求來。

$provider = $this->getRequestParameter('provider', $request);
if(is_null($provider) && is_null($username)){
    throw OAuthServerException::invalidRequest('username');
}

3.2 還需要擴充套件 vendor/laravel/passport/src/Bridge/UserRepository.php 裡面的 getUserEntityByUserCredentials,在App\Auth新建MuitiAuthPassportRepository.php,程式碼如下:

<?php

namespace App\Auth;

use App;
use Illuminate\Http\Request;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Laravel\Passport\Bridge\UserRepository;
use Laravel\Passport\Bridge\User;
use RuntimeException;

class MuitiAuthPassportRepository extends UserRepository
{

    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $provider = config('auth.guards.api.provider');

        if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
            throw new RuntimeException('Unable to determine authentication model from configuration.');
        }

        if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            if (method_exists($model, 'findForPassportMulti')) {
                $user = (new $model)->findForPassportMulti(App::make(Request::class)->all());
            }else{
                $user = (new $model)->where('email', $username)->first();
            }
        }

        if (! $user) {
            return;
        } elseif (method_exists($user, 'validateForPassportPasswordGrant')) {
            if (! $user->validateForPassportPasswordGrant($password)) {
                return;
            }
        } elseif (! $this->hasher->check($password, $user->getAuthPassword())) {
            return;
        }

        return new User($user->getAuthIdentifier());
    }

}

驗證傳遞引數的時候,如果模型裡面存在自定義的 findForPassportMulti 函式,則把所有的 Request 都傳遞進去,再多欄位也不怕!!!

if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            if (method_exists($model, 'findForPassportMulti')) {
                $user = (new $model)->findForPassportMulti(App::make(Request::class)->all());
            }else{
                $user = (new $model)->where('email', $username)->first();
            }
        }

3.3 修改Student Model:

<?php

namespace App\Models;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Student extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'school_id', 'student_no', 'password',
    ];

    protected $hidden = [
        'password'
    ];

    public function findForPassportMulti($request)
    {
        return $this->where('school_id', $request['school_id'])->where('student_no', $request['student_no'])->first();
    }

}

把所有的 Request 都傳遞進去以外,應該還有一種更優雅的實現邏輯,參考 Laravel 使用者多欄位認證優雅解決方案

3.4 在 App\Providers 新建 PassportServiceProvider.php,引入上面的擴充套件

<?php

namespace App\Providers;

use App\Auth\MuitiAuthPassportRepository;
use App\Auth\PasswordGrant;
// use League\OAuth2\Server\Grant\PasswordGrant;
use Laravel\Passport\PassportServiceProvider as BasePassportServiceProvider;
use Laravel\Passport\Passport;

class PassportServiceProvider extends BasePassportServiceProvider
{
    /**
     * Create and configure a Password grant instance.
     *
     * @return PasswordGrant
     */
    protected function makePasswordGrant()
    {
        $grant = new PasswordGrant(
            $this->app->make(MuitiAuthPassportRepository::class),
            // $this->app->make(\Laravel\Passport\Bridge\UserRepository::class),
            $this->app->make(\Laravel\Passport\Bridge\RefreshTokenRepository::class)
        );

        $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());

        return $grant;
    }

}

這個 PassportServiceProvider 繼承了 Passport 原有的 PassportServiceProvider,需要手動加入 config/app.php的providers配置段中:

'providers' => [
        ...
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\PassportServiceProvider::class,
    ],

測試一下多欄位的登入,正常返回Token.:

curl --request POST \
  --url http://multiauth.test/oauth/token \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'school_id=10001&student_no=17000003001&password=st001&client_id=2&client_secret=secret&grant_type=password&scope=&provider=students'

如果少傳一個欄位student_no:

curl --request POST \
  --url http://multiauth.test/oauth/token \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'school_id=10001&password=st001&client_id=2&client_secret=secret&grant_type=password&scope=&provider=students'

則妥妥的報錯:Undefined index: student_no

Laravel的異常丟擲有更優雅的實現方式,不在本文討論範圍內。

至此,多欄位登入的功能也實現了。

下一章講解退出登入以及通過Middleware限制訪問。

相關文章