3. 多欄位登入
傳統意義的多欄位登入,是通過 userid 或 username 或 email 這類任意一個 unique 欄位+密碼驗證。例如京東:
通過 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。
以下部分程式碼參考了@Ryan 的 Laravel 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限制訪問。