Laravel Passport 多表用户认证踩坑

本次项目的后台使用 iview-admin 搭建,前后端是分离的,需要给后台管理也写一套接口(看了几篇资料说可以把iview-admin 放到 laravel 项目里跑,但没有成功,卒),于是想用 passport 来做后台接口的认证。谁知过程一波三折,折腾了两天,算是摸清了坑也爬起来了。坑了一次不能再被坑第二次,也不希望后面的人接着被坑,于是就有了这篇文章,给大家介绍一下整体的流程和避坑操作。

1. 安装

项目是 Laravel 5.5 的,直接使用 composer require laravel/passport 安装可能会出现报错。如果报错的话建议修改 composer.json 文件,然后 composer update 安装。

"require": {
    "laravel/passport": "~4.0"
}

2. 配置

执行迁移

框架会自动生成 passport 所需要的数据表

$ php artisan migrate

生成加密密钥

$ php artisan passport:install

使用 HasApiTokens

在认证用的 model 中添加 HasApiTokens Trait,并且 model 要继承 Illuminate\Foundation\Auth\User

<?php

namespace App\Models;

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

class AdminUser extends Authenticatable
{
    use HasApiTokens;
}

更改 guard 和 provider

config/auth.php

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

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

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

    'admin_users' => [
        'driver' => 'eloquent',
        'model' => App\Models\AdminUser::class,
    ],
],

3. 使用

到这一步,可以开始实现登录的逻辑了,我希望验证的是 admin_users 表里的管理用户,但是根据其他教程的配置,发现一直验证的都是 users 表。最后看了源码,发现这么一个大坑。
src/Bridge/UserRepository.php

public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
{
    $provider = config('auth.guards.api.provider');

    if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
        throw new RuntimeException('Unable to determine authentication model from configuration.');
    }

    if (method_exists($model, 'findForPassport')) {
        $user = (new $model)->findForPassport($username);
    } else {
        $user = (new $model)->where('email', $username)->first();
    }
    ·
    ·
    ·
}

这段源码可以发现两个问题。第一个,passport 认证的 model 是 auth.guards.api.provider 里定义的 model。
第二个,认证默认验证的字段是 email,如果你的用户名是其他的字段,那么就要自己在认证的model里实现 findForPassport 方法。发现了坑,就想办法解决掉,下面是具体的使用方法,可以供大家参考。

创建一个登录认证的 trait

<?php
/**
 * Created by PhpStorm.
 * User: jason
 * Date: 2018/12/24
 * Time: 0:14
 */

namespace App\Http\Controllers\Admin\Traits;

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

trait ProxyHelpers
{
    /**
     * @return mixed
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function authenticate($guard = '')
    {
        $client = new Client();

        try {
            $url = request()->root() . '/admin/oauth/token';

            $params = array_merge(config('passport.proxy'), [
                'username' => request('username'),
                'password' => request('password'),
                'provider' => $guard
            ]);

            $respond = $client->post($url, ['form_params' => $params]);
        } catch (RequestException $exception) {
            abort(401, $exception->getMessage());
        }

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

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

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

        try {
            $url = request()->root() . 'admin/oauth/token';

            $params = array_merge(config('passport.proxy'), [
                'refresh_token' => request('refresh_token'),
            ]);

            $respond = $client->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 错误');
    }
}

登录控制器

使用上面定义的 ProxyHelpers trait 来完成认证,响应部分可以参考 Laravel5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 的内容来实现

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Admin\Traits\ProxyHelpers;
use App\Http\Requests\Admin\LoginRequest;
use App\Models\AdminUser;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class LoginController extends Controller
{
    use AuthenticatesUsers, ProxyHelpers;

    public function login(LoginRequest $request)
    {
        $user = AdminUser::query()->where('username', $request->username)->first();

        if (!$user) {
            return $this->failed('用户不存在', 401);
        }
        if (!Hash::check($request->password, $user->password)) {
            return $this->failed('密码不正确');
        }

        $user->last_login_at = Carbon::now()->toDateTimeString();
        $user->save();

        $token = $this->authenticate(‘admin’);  // 认证用的 guard
        return $this->success(['token' => $token, 'user' => $user]);
    }

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

创建一个处理请求的中间件 PassportCustomProvider

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Config;

class PassportCustomProvider
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $params = $request->all();
        if (array_key_exists('provider', $params)) {
            Config::set('auth.guards.api.provider', $params['provider']);  // 动态配置 auth.guards.api.provider 的 model
        }
        return $next($request);
    }
}

注册路由中间件

app\Http\Kernel.php 中注册配置的路由

protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'passport-administrators' => \App\Http\Middleware\PassportCustomProvider::class
];

配置 passport 路由

app/Providers/AuthServiceProvider.php

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->registerPolicies();

        // 我只需要前后端分离的 password 授权模式,所以只注册获取 Token 的路由
        Passport::routes(function(RouteRegistrar $router) {
            $router->forAccessTokens();
        }, ['prefix' => 'admin/oauth', 'middleware' => 'passport-administrators']);
    }
}

最后配置 LoginController 的路由,然后就发起请求吧!

参考资料

  1. passport API 认证 -- 多表登录
  2. Laravel Passport 踩坑日记
  3. Laravel 5.5 使用 Passport 实现 Auth 认证
  4. Laravel5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
JasonG
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 7
php炎黄

能不用passport就不要用了

5年前 评论
zhi8023nan 4年前

@php炎黄 遇到过什么坑吗?

5年前 评论

access_token 只记录了用户表的 user_id,并没有记录用户表,验证 token 时还是调用 auth.guards.api.provider 配置的用户表,当 token 验证通过后获取认证用户信息时会有点问题

4年前 评论
wonbin

核心思想就是在 中间件 PassportCustomProvider 中动态替换 Config::set('auth.guards.api.provider', $params['provider']); 的值,并对相应的路由组进行封装替换掉 默认的

4年前 评论

passport.proxy是啥配置,这个贴一下呀

3年前 评论

另外,其实你那个 ProxyHelpers 没太看懂,能说明一下吗

3年前 评论

新版本的Passport在创建客户端的时候可以指定客户端使用哪张表格做用户验证,现在只需要在用户表上加上HasApiTokens然后覆盖findForPassport($username)方法就可以了。

3年前 评论
xujinhuan 3年前

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