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
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 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年前

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