Laravel passport 多端用户使用

Passport

说明

使用 passport 进行 admin 端和 customer 端的用户认证。

虽然教程很多,但是我并没有参照其他教程完整的走下来,所以记录了自己的开发流程,希望能对其他人有所帮助。

github

安装项目

laravel new passport

安装 passport

composer require laravel/passport

xn7Qi1hF8X.png!large

数据迁移

首先我们需要创建 admins 和 customers 表,并填充假数据


php artisan make:migration create_admins_table --create=admins

php artisan make:migration create_customers_table --create=customers

php artisan make:seeder AdminsTableSeeder

php artisan make:seeder CustomersTableSeeder

pP2utXzfEm.png!large0bNzNqD1hv.png!large6r5x6i6rJ4.png!largeGX2ljwUzdx.png!largeKOgzzzC5u9.png!large9Cfex3Trxz.png!large

执行迁移

php artisan migrate --seed

IfnofuBEae.png!large

passport 初始化

php artisan passport:install

qZeg3ZjtjZ.png!large

此时在 storage 下会生成 oauth-private.key 和 oauth-public.key

5WfwipHHnA.png!large

生成认证

目前我们只是为前后端分离的后台使用,所以 password 模式足够

php artisan passport:client --password --name='passport-admin'

php artisan passport:client --password --name='passport-customer'

ndeZJKSbCz.png!large

备注

原先我以为这里采用不一样的数据之后下面 token 不会出现复用的情况,然而和这个没有关系

token 复用是指 admin 端生成的 1 号用户的 token 去请求 customer 端时,依然有效

解决方法下文会有介绍

修改路由配置

找到 app/Providers/RouteServiceProvider.php, 增加如下代码


 public function map()
    {
        ·
        ·
        ·

        // admin 路由
        $this->mapAdminRoutes();
        // customer 路由
        $this->mapCustomerRoutes();
    }

    protected function mapAdminRoutes()
    {
        Route::prefix('admin')
            ->namespace($this->namespace . '\Admin')
            ->group(base_path('routes/admin.php'));
    }

    protected function mapCustomerRoutes()
    {
        Route::prefix('customer')
            ->namespace($this->namespace . '\Customer')
            ->group(base_path('routes/customer.php'));
    }

在 routes 下新建 admin.php 和 customer.php

分别增加如下代码

admin.php


<?php

Route::group([
    'middleware' => 'passport-guard'
], function () {
    // 登录
    Route::post('login', 'AuthController@login');
    // 刷新 token
    Route::put('refresh', 'AuthController@refresh');
    Route::group([
        'middleware' => ['auth:api', 'scopes:admin']
    ], function () {
        // 退出
        Route::delete('logout', 'AuthController@logout');
        // 详情
        Route::get('admins/current', 'AdminsController@current');
    });
});

customer.php


<?php

Route::group([
    'middleware' => 'passport-guard'
], function () {
    // 登录
    Route::post('login', 'AuthController@login');
    // 刷新 token
    Route::put('refresh', 'AuthController@refresh');
    Route::group([
        'middleware' => ['auth:api', 'scopes:customer']
    ], function () {
        // 退出
        Route::delete('logout', 'AuthController@logout');
        // 详情
        Route::get('customers/current', 'CustomersController@current');
    });
});

备注

此处 auth:api 是检验 token

scopes:admin, scopes:customer 是给 token 指定作用域,即防止上文 token 复用的情况出现

创建中间件

php artisan make:middleware PassportGuard

F3s7tI3Ubo.png!large

增加如下代码

因为 passport 默认使用的是 api 守卫,并且不支持传参修改,所以需要通过中间件修改 provider

 public function handle($request, Closure $next)
 {

        try {
            if ($request->is('admin/*')) {// 如果是 admin 路由
                config(['auth.guards.api.provider' => 'admins']);
            } elseif ($request->is('customer/*')) { // 如果是 customer 路由
                config(['auth.guards.api.provider' => 'customers']);
            }
        } catch (\Exception $exception) {
            throw new $exception;
        }

        return $next($request);
}

找到 app/Http/Kernel.php,注册中间件

 protected $routeMiddleware = [
        ·
        ·
        ·
        // passport 认证路由
        'passport-guard' => \App\Http\Middleware\PassportGuard::class
       // token 作用域
        'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
        'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
    ];

修改 Providers

找到 app/Http/Providers

增加如下代码

use Laravel\Passport\Passport;
use Laravel\Passport\RouteRegistrar;

public function boot()
    {
        ·
        ·
        ·
        // Passport 路由注册
        $prefix = '';
        if (request()->is('admin/*')) {
            $prefix = 'admin';
        } elseif (request()->is('customer/*')) {
            $prefix = 'customer';
        }

        // 我们只需要前后端分离的形式, 而不需要认证
        Passport::routes(function (RouteRegistrar $router) {
            $router->forAccessTokens();
        }, ['prefix' => $prefix . '/oauth', 'middleware' => 'passport-guard']);
        // token 作用域
        Passport::tokensCan([
            'admin' => 'admin',
            'customer' => 'customer'
        ]);
         // access_token 过期时间
        Passport::tokensExpireIn(Carbon::now()->addDays(15));
        // refreshTokens 过期时间
        Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
    }

此时还需修改 config/auth.php, 修改为如下代码

'guards' => [
        ·
        ·
        ·

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],

        'admin' => [
            'driver' => 'passport',
            'provider' => 'admins',
        ],

        'customer' => [
            'driver' => 'passport',
            'provider' => 'customers',
        ],
    ],

  'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],

        'customers' => [
            'driver' => 'eloquent',
            'model' => App\Customer::class,
        ],
    ],

创建 Model

php artisan make:model Admin
php artisan make:model Customer

xRlk0O5hDa.png!large

分别修改为如下代码

Admin.php


<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

class Admin extends Authenticatable
{
    use HasApiTokens;

    /**
     * Passport 多认证字段
     */
    public function findForPassport($username)
    {
        return self::orWhere('email', $username)->orWhere('username', $username)->first();
    }
}

Customer.php


<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class Customer extends Authenticatable
{
    use HasApiTokens;

    /**
     * Passport 多认证字段
     */
    public function findForPassport($username)
    {
        return self::orWhere('email', $username)->orWhere('username', $username)->first();
    }
}

创建控制器

php artisan make:controller Admin/AuthController
php artisan make:controller Admin/AdminsController
php artisan make:controller Customer/AuthController
php artisan make:controller Customer/CustomersController

p49WSdebVj.png!large

安装 guzzle 扩展包

composer require guzzlehttp/guzzle

在 app/Http/Controllers/Admin 下新建 Traits/TokenTrait,新增如下代码

<?php

namespace App\Http\Controllers\Admin\Traits;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

trait TokenTrait
{
    public function authenticate()
    {
        $client = new Client();

        try {
            // 请求本地的 passport token
            $url = request()->root() . '/admin/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
            $params = [
                'grant_type' => 'password', // 认证类型 passport
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'admin', // 设置 token 作用域
                'username' => request('username'),
                'password' => request('password'),
            ];

            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系统异常');
        }

        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }

        abort(401, '账号或密码错误');
    }

    public function getRefreshToken()
    {
        $client = new Client();

        try {
            // 请求本地的 passport token
            $url = request()->root() . '/admin/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
            $params = [
                'grant_type' => 'refresh_token',// 认证类型 refresh_token
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'admin', // 设置 token 作用域
                'refresh_token' => request('refresh_token')
            ];
            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系统异常');
        }
        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }
        abort(401, 'refresh token 错误');
    }
}

在 app/Http/Controllers/Customer 下新建 Traits/TokenTrait,新增如下代码


namespace App\Http\Controllers\Customer\Traits;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

trait TokenTrait
{
    public function authenticate()
    {
        $client = new Client();

        try {
            $url = request()->root() . '/customer/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
            $params = [
                'grant_type' => 'password', // 认证类型 passport
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'customer', // 设置 token 作用域
                'username' => request('username'),
                'password' => request('password'),
            ];

            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系统异常');
        }

        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }

        abort(401, '账号或密码错误');
    }

    public function getRefreshToken()
    {
        $client = new Client();

        try {
            // 请求本地的 passport token
            $url = request()->root() . '/customer/oauth/token';
            $password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
            $params = [
                'grant_type' => 'refresh_token',// 认证类型 refresh_token
                'client_id' => $password_client->id,
                'client_secret' => $password_client->secret,
                'scope' => 'customer', // 设置 token 作用域
                'refresh_token' => request('refresh_token')
            ];
            $respond = $client->request('POST', $url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, '系统异常');
        }
        if ($respond->getStatusCode() !== 401) {
            return json_decode($respond->getBody()->getContents(), true);
        }
        abort(401, 'refresh token 错误');
    }
}

备注

其实此处请求的时候是有点问题的,用 guzzle 请求时如果不正确的参数是会返回 http 401 状态码以及报错,然后 guzzle 如果不是 http 200 的返回,都是会抛出异常的

所以此处抛出的异常更准确的是 账号密码或 refresh_token 错误,最下面的 abort() 也是不会执行的。

在 app/Http/Controllers/Admin 下新建 Controller.php,新增如下代码

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller as BaseController;

class Controller extends BaseController
{

}

在 app/Http/Controllers/Customer 下新建 Controller.php,新增如下代码

<?php

namespace App.ttp.ontrollers.ustomer;

use App.ttp.ontrollers.ontroller as BaseController;

class Controller extends BaseController

{

}

修改 app/Http/Controller/Admin/AuthController.php 为如下代码

<?php

namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;

use App\Admin;
use App\Http\Controllers\Admin\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    use TokenTrait;

    public function login(Request $request)
    {
        // 根据用户名或者邮箱登录
        $admin = Admin::orWhere('username', $request->username)
            ->orwhere('email', $request->username)
            ->firstOrFail();

        // 检验密码是否正确,错误返回 401 和报错信息
        if (!Hash::check($request->password, $admin->password)) {
            return response()->json([
                'message' => '用户名或密码错误'
            ], 401);
        }

        $token = $this->authenticate();
        return response()->json($token);
    }

    public function refresh()
    {
        // 获取 token
        $token = $this->getRefreshToken();
        return response()->json($token);
    }

    public function logout()
    {
        if (Auth::guard('admin')->check()) {
            Auth::guard('admin')->user()->token()->delete();
        }

        return  response()->noContent();
    }
}

修改 app/Http/Controller/Customer/AuthController.php 为如下代码

<?php

namespace App\Http\Controllers\Customer;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

use App\Customer;
use App\Http\Controllers\Customer\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
    use TokenTrait;

    public function login(Request $request)
    {
        // 根据用户名或者邮箱登录
        $customer = Customer::orWhere('username', $request->username)
            ->orwhere('email', $request->username)
            ->firstOrFail();

        // 检验密码是否正确,错误返回 401 和报错信息
        if (!Hash::check($request->password, $customer->password)) {
            return response()->json([
                'message' => '用户名或密码错误'
            ], 401);
        }

        $token = $this->authenticate();
        return response()->json($token);
    }

    public function refresh()
    {
        // 获取 token
        $token = $this->getRefreshToken();
        return response()->json($token);
    }

    public function logout()
    {
        if (Auth::guard('customer')->check()) {
            Auth::guard('customer')->user()->token()->delete();
        }

        return  response()->noContent();
    }
}

好了,激动人心的时刻到了!

打开 postman 测试, 我分别创建了 6 个请求,具体看 url 和参数应该能明白

2lbONWWfDg.png!largeJJTjBhtls6.png!largeIgxZis6ah2.png!largeNT5DTYaShW.png!largep8Rq9va6ys.png!large9t9y3UBIyb.png!largeUBvu4gDEmG.png!large

ok,按照预期的该返回的返回,不通过的也没通过

接下来我们用这些 token 获取用户信息

p54YTCOupW.png!largeK6sF5dKXdR.png!largehxE2SR69nR.png!largeMwHzY22Pec.png!large1JQavAbVSk.png!largezpdtODkDK8.png!large

图 5 为我用 admin1 的 token 请求 customer 的接口

图 6 为我用 customer1 的 token 请求 admin 的接口

都是无效的。

接下来验证刷新 token

admin1 的 refresh_token

hHj4dVzNQ5.png!large

再请求一下

KEY0x47SeD.png!large

customer1 的 refersh_token

WAv3vnbJrG.png!largejZXsOS83oX.png!large

ok, 完工!

总结

  • 虽然 refresh_token 不能重新刷出来,但是之前没过期的 access_token 其实依然会有效

  • 个人觉得这个并不如 dingoapi + jwt (也就是第三本 api 的教程)好用,本文只说明怎么使用 passport 进行多端验证。像抛出异常,没有自定义返回码等还有一大堆未完善的东西。

参考资料

博客:Laravel5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 重点感谢

博客:Laravel Passport API 认证使用小结

博客:Laravel Passport 多表用户认证踩坑

博客:passport API 认证 -- 多表登录

博客:Laravel 5.5 使用 Passport 实现 Auth 认证

Passport OAuth 认证

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

内容挺好的,刚好是我需要的,要是能把文章格式调一下就好了 :+1:

4年前 评论
____ (楼主) 4年前

用laravel 8.x认证报错为:

Client error: POST http://laravel-passport-oauth.loc/admin/oauth/token resulted in a 400 Bad Request response: {"error":"invalid_grant","error_description":"The user credentials were incorrect.","message":"The user credentials were (truncated...)

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!