【问题标题】:How do we implement custom API-only authentication in Laravel我们如何在 Laravel 中实现自定义的仅 API 身份验证
【发布时间】:2020-09-10 19:04:42
【问题描述】:

这不是一个非常需要答案的问题,但欢迎提供进一步的建议和答案和建议。我想与全世界分享我是如何解决这个问题的,并希望它对其他人有所帮助。

Laravel 附带了几个预先设计的身份验证解决方案,您可以使用一些工匠命令来启动它们。其中包括:

  • 标准用户表身份验证
  • OAuth2(通过 Laravel Passport 包)
  • 基于社交媒体的身份验证(通过 Laravel Socialite 包)

尽管所有这些都很有用,但在这个微服务时代,Laravel 并没有为使用自定义 API 的纯 API 身份验证提供开箱即用的引导程序。

几个月前我遇到了这个问题,我在 Google 和 Stackoverflow 上搜索了答案。我找到了有助于指明方向的有用文章,并引用了这些文章。需要一些努力才能了解如何将它们粘合在一起并逐步调试以消除问题。

提供答案是希望它能帮助其他人 - 以及我自己,我将来必须再次做同样的事情。

假设和范围:

  • 您已经创建了自己的 API,例如 https://example.com/loginhttps://example.com/logout
  • 您运行的网站需要身份验证,但不需要通过模型和表格或社交媒体进行验证
  • 您的 API 管理与表的交互,包括用户登录/注销
  • 您使用 Laravel Passport 插件进行 OAuth2 身份验证(感谢 @ShuvoJoseph 让我注意到这一点)

【问题讨论】:

    标签: php laravel api authentication


    【解决方案1】:

    解决方案涉及七个PHP文件

    • app/Http/Controllers/HomeController.php - 主页控制器;经过身份验证的用户的目的地
    • app/Providers/ApiUserProvider.php - 用于引导和注册登录用户的自定义提供程序,并实现接口 Illuminate\Contracts\Auth\UserProvider
    • app/CoreExtensions/SessionGuardExtended.php - 自定义守卫控制器,用于登录用户并接收身份验证值并将它们存储在会话数组中;扩展类 Illuminate\Auth\SessionGuard
    • app/ApiUser - 如果你使用的是 OAuth2(Laravel 的 Passport);公开 OAuth access_token 的自定义用户类;扩展 Illuminate\Auth\GenericUser 并实现接口 Illuminate\Contracts\Auth\Authenticatable
    • config/auth.php - 指示 Auth() 门面返回自定义会话保护的身份验证配置
    • app/Providers/AuthServiceProvider.php - 身份验证引导
    • app/Providers/AppServiceProvider.php - 主应用程序引导

    引用源研究/调查材料供您自己调查并理解它们存在的背景背景。我没有声称自己是通过我自己的魔力从头开始创造解决方案的天才,而是——像所有创新者一样——我建立在其他人的努力之上。我的文章的独特卖点是我提供了一个完整的打包解决方案,而引用的来源为整体答案的特定部分提供了解决方案。经过多次尝试和错误,他们一起帮助我形成了一个完整的解决方案。

    了解 config/auth.php 如何影响 AuthManager.php 执行的一篇非常有用的文章是 https://www.2hatslogic.com/blog/laravel-custom-authentication/

    未对以下代码进行任何修改,但包括在内是为了承认它们所扮演的角色及其在此过程中的重要性:

    • vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php - 主要授权工厂管理器
    • Auth() 外观 - 默认情况下返回收缩包装的 Illuminate\Auth\SessionGuard 类实例,除非通过 config/auth.php 文件指示它不这样做 - Auth() 在整个 Laravel 代码中普遍用于检索会话守卫

    代码

    app/Http/Controllers/HomeController.php

    <?php
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    
    /**
     * Handles and manages the home-page
     * 
     * @category controllers
     */
    class HomeController extends Controller
    {
        /**
         * Create a new controller instance.
         *
         * @return void
         */
        public function __construct()
        {
            $this->middleware('auth');
        }
    
        public function index()
        {
            blah
        }
    
        ... other methods ... 
    
    }
    

    app/Providers/ApiUserProvider.php

    来源:

    <?php
    namespace App\Providers;
    
    use Illuminate\Contracts\Auth\UserProvider;
    use Illuminate\Contracts\Auth\Authenticatable as UserContract;
    use App\ApiUser;
    
    /**
     * Delegates API user login and authentication
     * 
     * @category providers
     */
    class ApiUserProvider implements UserProvider
    {
        
        /**
         * Custom API Handler 
         * Used to request API and capture responses
         * 
         * @var \Path\To\Your\Internal\Api\Handler
         */
        private $_oApi = null;
        
        /**
         * POST request to API
         * 
         * @param string  $p_url      Endpoint URL
         * @param array   $p_arrParam Parameters
         * @param boolean $p_isOAuth2 Is OAuth2 authenticated request? [Optional, Default=True]
         * 
         * @return array
         */
        private function _post(string $p_url, array $p_arrParam, bool $p_isOAuth2=true)
        {
            if (!$this->_oApi) {
                $this->_oApi = new \Path\To\Your\Internal\Api\Handler();
            }
            $arrResponse = $this->_oApi->post($p_url, $p_arrParam, $p_isOAuth2);
            return $arrResponse;
        }
        
        /**
         * GET request to API
         * 
         * @param string $p_url     Endpoint URL
         * @param array $p_arrParam Parameters [Optional, Default = array()]
         * 
         * @return array
         */
        private function _get(string $p_url, array $p_arrParam=[], bool $p_isOAuth2=true)
        {   
            if (!$this->_oApi) {
                $this->_oApi = new \Path\To\Your\Internal\Api\Handler();
            }
            $arrResponse = $this->_oApi->get($p_url, $p_arrParam);
            return $arrResponse;
        }
        
        /**
         * Retrieve a user by the given credentials.
         *
         * @param array $p_arrCredentials
         * 
         * @return \Illuminate\Contracts\Auth\Authenticatable|null
         */
        public function retrieveByCredentials(array $p_arrCredentials)
        {
            $arrResponse = $this->_post('/login', $p_arrCredentials, false);
            if ( $arrResponse['result'] ) {
                $arrPayload = array_merge(
                    $arrResponse['data'],
                    $p_arrCredentials
                );
                return $this->getApiUser($arrPayload);
            }
        }
    
        /**
         * Retrieve a user by their unique identifier.
         *
         * @param mixed $p_id
         * 
         * @return \Illuminate\Contracts\Auth\Authenticatable|null
         */
        public function retrieveById($p_id)
        {
            $arrResponse = $this->_get("user/id/{$p_id}");        
            if ( $arrResponse['result'] ) {
                return $this->getApiUser($arrResponse['data']);
            }
        }
    
        /**
         * Validate a user against the given credentials.
         *
         * @param \Illuminate\Contracts\Auth\Authenticatable $p_oUser
         * @param array                                      $p_arrCredentials
         * 
         * @return bool
         */
        public function validateCredentials(UserContract $p_oUser, array $p_arrCredentials)
        {
            return $p_oUser->getAuthPassword() == $p_arrCredentials['password'];
        }
    
        /**
         * Get the api user.
         *
         * @param mixed $p_user
         * 
         * @return \App\Auth\ApiUser|null
         */
        protected function getApiUser($p_user)
        {
            if ($p_user !== null) {
                return new ApiUser($p_user);
            }
            return null;
        }
    
        protected function getUserById($id)
        {
            $user = [];
    
            foreach ($this->getUsers() as $item) {
                if ($item['account_id'] == $id) {
                    $user = $item;
    
                    break;
                }
            }
    
            return $user ?: null;
        }
    
        protected function getUserByUsername($username)
        {
            $user = [];
    
            foreach ($this->getUsers() as $item) {
                if ($item['email_address'] == $username) {
                    $user = $item;
    
                    break;
                }
            }
    
            return $user ?: null;
        }
        
    
        /**
         * The methods below need to be defined because of the Authenticatable contract
         * but need no implementation for 'Auth::attempt' to work and can be implemented
         * if you need their functionality
         */
        public function retrieveByToken($identifier, $token) { }
        public function updateRememberToken(UserContract $user, $token) { }
        
    }
    

    app/CoreExtensions/SessionGuardExtended.php

    来源:

    <?php
    namespace App\CoreExtensions;
    
    use Illuminate\Auth\SessionGuard;
    use Illuminate\Contracts\Auth\Authenticatable;
    
    /**
     * Extended SessionGuard() functionality 
     * Provides added functionality to store the OAuth tokens in the session for later use
     * 
     * @category guards
     * 
     * @see https://stackoverflow.com/questions/36087061/extending-laravel-5-2-sessionguard
     */
    class SessionGuardExtended extends SessionGuard
    {
        
        /**
         * Log a user into the application.
         *
         * @param  \Illuminate\Contracts\Auth\Authenticatable  $p_oUser
         * @param  bool  $p_remember
         * @return void
         */
        public function login(Authenticatable $p_oUser, $p_remember = false)
        {
            
            parent::login($p_oUser, $p_remember);
            
            /**
             * Writing the OAuth tokens to the session
             */
            $key = 'authtokens';
            $this->session->put(
                $key, 
                [
                    'access_token' => $p_oUser->getAccessToken(),
                    'refresh_token' => $p_oUser->getRefreshToken(),
                ]
            );
        }
        
        /**
         * Log the user out of the application.
         *
         * @return void
         */
        public function logout()
        {
            parent::logout();
            
            /**
             * Deleting the OAuth tokens from the session
             */
            $this->session->forget('authtokens');        
        }
        
    }
    

    应用程序/ApiUser

    来源:

    <?php
    namespace App;
    
    use Illuminate\Auth\GenericUser;
    use Illuminate\Contracts\Auth\Authenticatable as UserContract;
    
    class ApiUser extends GenericUser implements UserContract
    {
        
        /**
         * Returns the OAuth access_token
         * 
         * @return mixed
         */
        public function getAccessToken()
        {
            return $this->attributes['access_token'];
        }
        
        
        public function getRefreshToken()
        {
            return $this->attributes['refresh_token'];
        }
        
    }
    

    app/Providers/AuthServiceProvider.php

    <?php
    namespace App\Providers;
    
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
    
    class AuthServiceProvider extends ServiceProvider
    {
        
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
            
            Auth::provider('frank_sinatra', function ($app, array $config) {
                // Return an instance of Illuminate\Contracts\Auth\UserProvider...
    
                return new ApiUserProvider();
            });
            
        }
    }
    
    

    app/Providers/AppServiceProvider.php

    来源:

    注意:

    在此 PHP 文件中对编码的更改存在一些细微的问题。 如果您想了解更多,请查看 vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php,特别是 AuthManager::resolve()。

    1. 对 config/auth.php 'session' 和 'token' 的引用由硬编码方法 AuthManager::createSessionDriver() 和 AuthManager::createTokenDriver() 提供 (如果您知道在应用程序中扩展 AuthManager.php 的方法,请告诉我)
    2. AppServiceProvider.php 来救援!可以在 AppServiceProvider::boot() 中注册自定义守卫,并在执行默认代码之前进行拦截。
    3. 我同意上面的第 2 点,但我们不能做一些聪明的事情,比如从 AppServiceProvider 返回自定义会话保护名称或实例,在专门的公共方法中设置 setCookieJar()、setDispatcher()、setRequest()在 AuthManager.php 中,可以挂接到 AppServiceProvider.php 中,也可以在 AuthManager.php 中创建自定义 session-guard 后由 config/auth.php 驱动执行?
    4. 没有 cookie 或会话,用户的身份不会通过重定向保留。解决此问题的唯一方法是在我们当前的解决方案中包含 AppServiceProvider 中的 setCookieJar()、setDispatcher() 和 setRequest()。
    <?php
    namespace App\Providers;
    
    use Illuminate\Support\ServiceProvider;
    use Illuminate\Support\Facades\Auth;
    use App\CoreExtensions\SessionGuardExtended;
    
    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            //
        }
    
        /**
         * Bootstrap any application services.
         * 
         * @see https://stackoverflow.com/questions/36087061/extending-laravel-5-2-sessionguard
         *
         * @return void
         */
        public function boot()
        {
            
            /**
             * Extending Illuminate\Auth\SessionGuard()
             * This is so we can store the OAuth tokens in the session
             */
            Auth::extend(
                'sessionExtended',
                function ($app) {
                
                    $guard = new SessionGuardExtended(
                        'sessionExtended', 
                        new ApiUserProvider(), 
                        app()->make('session.store'),
                        request()
                    );
                
                    // When using the remember me functionality of the authentication services we
                    // will need to be set the encryption instance of the guard, which allows
                    // secure, encrypted cookie values to get generated for those cookies.
                    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;
                }
            );
        }
    }
    

    config/auth.php

    来源:

    <?php
    
    return [
    
        /*
        |--------------------------------------------------------------------------
        | Authentication Defaults
        |--------------------------------------------------------------------------
        |
        | This option controls the default authentication "guard" and password
        | reset options for your application. You may change these defaults
        | as required, but they're a perfect start for most applications.
        |
        */
    
        'defaults' => [
            //'guard' => 'web', /** This refers to the settings under ['guards']['web'] */
            'guard' => 'webextended', /** This refers to the settings under ['guards']['webextended'] */
            'passwords' => 'users', /** This refers to the settings under ['passwords']['users'] */
        ],
    
        /*
        |--------------------------------------------------------------------------
        | Authentication Guards
        |--------------------------------------------------------------------------
        |
        | Next, you may define every authentication guard for your application.
        | Of course, a great default configuration has been defined for you
        | here which uses session storage and the Eloquent user provider.
        |
        | All authentication drivers have a user provider. This defines how the
        | users are actually retrieved out of your database or other storage
        | mechanisms used by this application to persist your user's data.
        |
        | Supported: "session", "token"
        |
        */
    
        'guards' => [
            'web' => [
                'driver' => 'session', /** This refers to Illuminate/Auth/SessionGuard */
                'provider' => 'users', /** This refers to the settings under ['providers']['users'] */
            ],
            
            'webextended' => [
                'driver' => 'sessionExtended', /** @see app/Providers/AppServiceProvider::boot() */
                'provider' => 'users', /** This refers to the settings under ['providers']['users'] */
            ],
    
            'api' => [
                'driver' => 'token', /** This refers to Illuminate/Auth/TokenGuard */
                'provider' => 'users',
                'hash' => false,
            ],
        ],
    
        /*
        |--------------------------------------------------------------------------
        | User Providers
        |--------------------------------------------------------------------------
        |
        | All authentication drivers have a user provider. This defines how the
        | users are actually retrieved out of your database or other storage
        | mechanisms used by this application to persist your user's data.
        |
        | If you have multiple user tables or models you may configure multiple
        | sources which represent each model / table. These sources may then
        | be assigned to any extra authentication guards you have defined.
        |
        | Supported: "database", "eloquent"
        |
        */
    
        'providers' => [
            'users' => [
                'driver' => 'frank_sinatra',  /** @see app/Providers/AuthServiceProvider::boot() */
                //'model' => App\User::class,
            ],
    
            // 'users' => [
            //     'driver' => 'database',
            //     'table' => 'users',
            // ],
        ],
    
        [
            blah
        ],
    
        [
            other settings
        ],
    
    ];
    
    

    如何使用此解决方案

    非常简单。整体方法没有变化。换句话说,我们使用 Auth() 门面。

    使用自定义 API 登录时/login?username=&lt;username&gt;&amp;password=&lt;password&gt;

    request()->flash();
    $arrData = request()->all();
    
    if ( Auth::attempt($arrData, true) ) {
        return redirect('home');
    } else  {
        return back()->withErrors(
            [
                'username' => "Those credentials can't be found",
                'password' => "Those credentials can't be found",
            ]
        );
    }
    

    使用自定义 API 注销时/logout

    Auth::logout();
    return redirect('home');
    

    【讨论】:

    • 我邀请任何人提交任何改进或指出我可能错过的解决方案。让我们把它钉在外面供大家使用。
    • 还有一点是整个练习引起了我的注意。虽然 Laravel 的配置方式使开发人员能够轻松地启动视图和服务,但很容易得出结论,通过 Laravel 框架的子系统包含改进的功能很简单,但这是一个错误的概念。引擎盖下的 Laravel 很复杂,需要努力去理解。 Laravel 的设施非常强大,但添加新设施或扩展现有设施远非简单。
    • 一切正常,没有错误。但是“Auth::attempt($arrData, true)”这一行总是返回false!我什至登录了retrieveByCredentials(array $credentials),为了成功登录,它正在返回ApiUser。但是 Auth::attempt 总是错误的。有什么建议吗?
    • @ShuvoJoseph 嗨 Shuvo,您是否在 config/auth.php 中将默认保护设置为“webextended”?
    • @ShuvoJoseph 您的问题立即向我表明,经过身份验证的用户未存储在会话中。会不会是这个问题?
    【解决方案2】:

    这种方法的问题在于它不处理密码重置,这需要将令牌存储在本地数据库中并且很难覆盖。

    【讨论】:

    • 同意。即便如此,我觉得这种方法需要记录在某个地方,因为我进行了大量研究以得出一个整体的单一解决方案。我想知道有一篇 StackOverflow 文章,如果有必要,我可以在某个时候参考。我的调查显示,系统依赖 API 响应登录而不是基于 db 的用户登录的情况比您想象的要多。
    • 我很高兴你做到了,你所做的帮助了我。我很惊讶没有人为 API 身份验证构建完整的 Laravel 脚手架 - 对我来说似乎是一个明显的用例。
    • 谢谢@diecheese。非常感激。请记住,虽然此解决方案主要实现 OAuth,但由于您提到的原因,这可能会很痛苦,但它可以适应其他身份验证方法。如果不直接查看代码,而是提出最佳猜测建议,我会说需要对 config/auth.php 以及我的原始解决方案中引用“access_token”和“refresh_token”的任何地方进行更改。整体解决方案架构将保持不变,而某些方法中的实现细节可能需要稍作更改。
    • 如果您已经基于我的原始帖子实施了一个解决方案,但正在修改我的原始帖子以使用另一种身份验证方法,那么 - 请! - 我邀请您将其发布为答案。如果它有帮助,并且如果写一篇大文章的前景让您不那么高兴,请参考我的答案中的组件以及您需要进行的修改。我知道这对于寻求解决问题的其他人来说是非常有价值的。
    猜你喜欢
    • 2018-04-09
    • 1970-01-01
    • 1970-01-01
    • 2015-02-23
    • 2020-04-24
    • 2016-07-24
    • 2019-12-14
    • 2015-10-13
    相关资源
    最近更新 更多