我们如何在 Laravel 中实现自定义的仅 API 身份验证

use*_*602 3 php authentication api laravel

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

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

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

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

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

提供答案是希望对其他人和我自己有所帮助,因为我将来必须再次做同样的事情。

假设和范围:

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

use*_*602 5

该解决方案涉及七个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 代码中普遍使用来检索会话保护

代码

应用程序/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 ... 

}
Run Code Online (Sandbox Code Playgroud)

应用程序/提供商/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) { }
    
}
Run Code Online (Sandbox Code Playgroud)

应用程序/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 /sf/ask/2526094301/
 */
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');        
    }
    
}
Run Code Online (Sandbox Code Playgroud)

应用程序/API用户

资料来源:

<?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'];
    }
    
}
Run Code Online (Sandbox Code Playgroud)

应用程序/提供商/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();
        });
        
    }
}

Run Code Online (Sandbox Code Playgroud)

应用程序/提供商/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 返回自定义会话保护名称或实例,在 AuthManager 中的专门公共方法中使用 setCookieJar()、setDispatcher()、setRequest() 。 php,它可以挂接到 AppServiceProvider.php 中,也可以由 config/auth.php 驱动在 AuthManager.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 /sf/ask/2526094301/
     *
     * @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;
            }
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

配置/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
    ],

];

Run Code Online (Sandbox Code Playgroud)

如何使用该解决方案

很简单。总体方法没有变化。换句话说,我们使用 Auth() 外观。

使用自定义 API 登录时/login?username=<username>&password=<password>

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",
        ]
    );
}
Run Code Online (Sandbox Code Playgroud)

使用自定义 API 注销时/logout

Auth::logout();
return redirect('home');
Run Code Online (Sandbox Code Playgroud)