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 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 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年前 评论

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