1. 程式人生 > >Laravel實現小程式使用openid登陸、手機號驗證碼登陸、賬戶密碼登陸三種登陸方式

Laravel實現小程式使用openid登陸、手機號驗證碼登陸、賬戶密碼登陸三種登陸方式

目前開發小程式,按需求要實現3種登陸方式:
1、微信授權登陸
2、賬戶密碼登陸
3、手機號、驗證碼登陸
我使用laravel自帶的Auth認證機制,通過attempt方法進行賬戶驗證,但是預設的認證機制必須包含password欄位,而我的第1、3種登陸方式都沒有password欄位,所以需要深入原始碼瞭解認證機制的實現,然後再進行修改。
首先,看看自帶的Auth功能的LoginController怎麼實現的:

class LoginController extends Controller
{
...
    use AuthenticatesUsers;
...
}

使用了trait:AuthenticatesUsers,AuthenticatesUsers中有一個login方法就是實現預設的登陸方式的方法:

    public function login(Request $request)
    {
        //這裡是對登陸引數做表單驗證
        $this->validateLogin($request);

		//這裡是防止暴力破解,對同一個IP的介面呼叫次數做限制
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);//限制訪問
            return $this->sendLockoutResponse
($request);//發回限制訪問的響應 } //驗證登陸 if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request);//返回登陸成功的響應 } //登入失敗,失敗次數++,防止暴力破解 $this->incrementLoginAttempts($request); // 返回登陸失敗的響應 return $this->sendFailedLoginResponse
($request); }

這裡的重點在於:attemptLogin方法的呼叫,這才是關鍵的一步:登陸驗證

    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }

再看guard函式:

    /**
     * Get the guard to be used during authentication.
     *
     * @return \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected function guard()
    {
        return Auth::guard();
    }

註釋說明返回

\Illuminate\Contracts\Auth\StatefulGuard

,找到該檔案發現這是一個介面檔案,定義 了attempt方法,直接搜尋

implements StatefulGuard

看哪個類實現了該介面,找到了

Illuminate\Auth\SessionGuard

以及其中的attempt方法:

    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array  $credentials
     * @param  bool   $remember
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);
		//這裡獲取了使用者資訊
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
		//校驗使用者密碼
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

        $this->fireFailedEvent($user, $credentials);

        return false;
    }

獲取使用者資訊:

 $user = $this->provider->retrieveByCredentials($credentials);

和校驗使用者密碼:

$this->hasValidCredentials($user, $credentials)

就是Auth認證的核心了,首先看怎麼獲取使用者資訊:

Illuminate\Auth\SessionGuard

的建構函式可見在例項化SessionGuard的時候傳入了UserProvider $provider:

    public function __construct($name,
                                UserProvider $provider,
                                Session $session,
                                Request $request = null)
    {
        $this->name = $name;
        $this->session = $session;
        $this->request = $request;
        $this->provider = $provider;
    }

直接搜尋

new SessionGuard

找到

Illuminate\Auth\AuthManager

中的:

    /**
     * Create a session based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\SessionGuard
     */
    public function createSessionDriver($name, $config)
    {
    	//看這裡,通過$config['provider']建立了provider
        $provider = $this->createUserProvider($config['provider'] ?? null);
        $guard = new SessionGuard($name, $provider, $this->app['session.store']);
        if (method_exists($guard, 'setCookieJar')) {
            $guard->setCookieJar($this->app['cookie']);
        }

        if (method_exists($guard, 'setDispatcher')) {
            $guard->setDispatcher($this->app['events']);
        }

        if (method_exists($guard, 'setRequest')) {
            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
        }

        return $guard;
    }

繼續跟蹤到Illuminate\Auth\AuthManager使用的trait:Illuminate\Auth\CreatesUserProviders中的createUserProvider:

    public function createUserProvider($provider = null)
    {
        if (is_null($config = $this->getProviderConfiguration($provider))) {
            return;
        }

        if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
            return call_user_func(
                $this->customProviderCreators[$driver], $this->app, $config
            );
        }

        switch ($driver) {
            case 'database':
                return $this->createDatabaseProvider($config);
            case 'eloquent':
                return $this->createEloquentProvider($config);
            default:
                throw new InvalidArgumentException(
                    "Authentication user provider [{$driver}] is not defined."
                );
        }
    }

對照config/auth.php中的provider驅動配置,預設是eloquent,也就是會執行:

return $this->createEloquentProvider($config);

跟棕到該方法:

    protected function createEloquentProvider($config)
    {
        return new EloquentUserProvider($this->app['hash'], $config['model']);
    }

可以確定在 Illuminate\Auth\SessionGuard的attempt函式中的provider就是Illuminate\Auth\EloquentUserProvider,找到retrieveByCredentials函式:

    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            array_key_exists('password', $credentials))) {
            return;
        }
        $query = $this->createModel()->newQuery();

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }

在這裡根據除密碼之外的其它引數查詢出了使用者資料。
回到 Illuminate\Auth\SessionGuard,再看:

    /**
     * Determine if the user matches the credentials.
     *
     * @param  mixed  $user
     * @param  array  $credentials
     * @return bool
     */
    protected function hasValidCredentials($user, $credentials)
    {
        return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
    }

呼叫了Illuminate\Auth\EloquentUserProvider的validateCredentials方法:


    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];
		//對比加密後的密碼是否和資料庫中的相同
        return $this->hasher->check($plain, $user->getAuthPassword());
    }

最終,我們確認只要在EloquentProvider中的validateCredentials修改為自己的驗證方式就可以實現需求了,可是直接修改原始碼還是不安全,可能會導致其它不可預測的問題,畢竟沒有深入研究,還是保險一點,增加一個provider,寫一個新的validateCredentials方法,會是更好的選擇。
新建一個NewEloquentUserProvider繼承EloquentUserProvider,重寫validateCredentials:

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        if(array_key_exists('openid',$credentials)){
            //openid登陸
            $openid = $credentials['openid'];
            if($user->getAuthOpenid() == $openid) return true;

        }elseif(array_key_exists('password',$credentials)){
            //Phone、password登陸
            $plain = $credentials['password'];
            return $this->hasher->check($plain, $user->getAuthPassword());

        }else{
            //Phone、code登陸
           $authCode  = Cache::get("login_verification_code_".$credentials['code']);
            if($authCode && $authCode == $credentials['code']) return true;

        }

        return false;
    }

實現三種方式的登陸驗證,然後在 trait:Illuminate\Auth\CreatesUserProviders中的createUserProvider函式的switch分支裡新增一個case,並返回NewEloquentUserProvider的例項,再將config/auth.php中的providers.users.driver配置改為該case的值即可。