Laravel Passport API 认证使用小结

看到Laravel-China社区常有人问Laravel Passport用于密码验证方式来获取Token的问题,刚好我最近一个API项目使用Laravel Dingo Api+Passport,也是使用Oauth2 的'grant_type' => 'password'密码授权来做Auth验证,对于如何做登录登出,以及多账号系统的认证等常用场景做一下简单的使用小总结。

基本配置

基本安装配置主要参照官方文档,具体不详细说,列出关键代码段

config/auth.php

'guards' => [
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

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

Providers/AuthServiceProvider.php

    public function boot()
    {
        $this->registerPolicies();

        //默认令牌发放的有效期是永久
        //Passport::tokensExpireIn(Carbon::now()->addDays(2));
        //Passport::refreshTokensExpireIn(Carbon::now()->addDays(4));
        Passport::routes(function (RouteRegistrar $router) {
            //对于密码授权的方式只要这几个路由就可以了
            config(['auth.guards.api.provider' => 'users']);
            $router->forAccessTokens();
        });
    }

Middleware/AuthenticateApi.php 自定义中间件返回

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Auth\Middleware\Authenticate;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class AuthenticateApi extends Authenticate
{
    protected function authenticate(array $guards)
    {

        if ($this->auth->guard('api')->check()) {
            return $this->auth->shouldUse('api');
        }

        throw new UnauthorizedHttpException('', 'Unauthenticated');
    }
}

App/Http/Kernel.php


    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'api-auth' => AuthenticateApi::class,
        ......
    ];
}

账号验证字段不止邮箱

对于账号验证不止是数据表中的emial字段,还可能是用户名或者手机号字段只需要在User模型中添加findForPassport方法,示例代码如下:

App\Models\Users

class User extends Authenticatable implements Transformable
{
    use TransformableTrait, HasApiTokens, SoftDeletes;
    public function findForPassport($login)
    {
        return $this->orWhere('email', $login)->orWhere('phone', $login)->first();
    }
}

客户端获取access_token请求只传用户名和密码

对于密码授权的方式需要提交的参数如下:

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '',
    ],
]);

但是客户端请求的时候不想把grant_type,client_id,client_secret,scope放到请求参数中或者暴露给客户端,只像JWT一样只发送usernamepassword 怎么办?很简单我们只要将不需要请求的放到配置文件中,然后客户端请求用户名密码以后我们再向oauth/token发送请求带上相关的配置就可以了。

.env.php

OAUTH_GRANT_TYPE=password
OAUTH_CLIENT_ID=1
OAUTH_CLIENT_SECRET=EvE4UPGc25TjXwv9Lmk432lpp7Uzb8G4fNJsyJ83
OAUTH_SCOPE=*

config/passport.php 当然该配置你可以配置多个client

return [
    'grant_type' => env('OAUTH_GRANT_TYPE'),
    'client_id' => env('OAUTH_CLIENT_ID'),
    'client_secret' => env('OAUTH_CLIENT_SECRET'),
    'scope' => env('OAUTH_SCOPE', '*'),
];

LoginController.php的示例代码如下,因为用了Dingo Api配置了api前缀,所以请求/api/oauth/token

    /**
     * 获取登录TOKEN
     * @param LoginRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function token(LoginRequest $request)
    {
        $username = $request->get('username');
        $user = User::orWhere('email', $username)->orWhere('phone', $username)->first();

        if ($user && ($user->status == 0)) {
            throw  new UnauthorizedHttpException('', '账号已被禁用');
        }

        $client = new Client();
        try {
            $request = $client->request('POST', request()->root() . '/api/oauth/token', [
                'form_params' => config('passport') + $request->only(array_keys($request->rules()))
            ]);

        } catch (RequestException $e) {
            throw  new UnauthorizedHttpException('', '账号验证失败');
        }

        if ($request->getStatusCode() == 401) {
            throw  new UnauthorizedHttpException('', '账号验证失败');
        }
        return response()->json($request->getBody()->getContents());
    }

退出登录并清除Token

对于客户端退出后并清除记录在oauth_access_tokens表中的记录,示例代码如下:

  /**
     * 退出登录
     */
    public function logout()
    {
        if (\Auth::guard('api')->check()) {
            \Auth::guard('api')->user()->token()->delete();
        }

        return response()->json(['message' => '登出成功', 'status_code' => 200, 'data' => null]);
    }

根据用户ID认证用户

app('auth')->guard('api')->setUser(User::find($userId));

多用户表(多Auth)认证

比如针对客户表和管理员表分别做Auth认证的情况,也列出关键代码段:

 'guards' => [
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],

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

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

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

新建一个PasspordAdminServiceProvider来实现我们自己的PasswordGrant,别忘了添加到config/app.phpproviders配置段中

AppProviders/PasspordAdminServiceProvider

<?php

namespace App\Providers;

use App\Foundation\Repository\AdminUserPassportRepository;
use League\OAuth2\Server\Grant\PasswordGrant;
use Laravel\Passport\PassportServiceProvider as BasePassportServiceProvider;
use Laravel\Passport\Passport;

class PasspordAdminServiceProvider extends BasePassportServiceProvider
{
    /**
     * Create and configure a Password grant instance.
     *
     * @return PasswordGrant
     */
    protected function makePasswordGrant()
    {
        $grant = new PasswordGrant(
            //主要是这里,我们调用我们自己UserRepository
            $this->app->make(AdminUserPassportRepository::class),
            $this->app->make(\Laravel\Passport\Bridge\RefreshTokenRepository::class)
        );

        $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());

        return $grant;
    }

}

新建AdminUserPassportRepository,Password的验证主要通过getUserEntityByUserCredentials,它读取配置的guards对应的provider来做认证,我们重写该方法,通过传递一个参数来告诉它我们要用哪个guard来做客户端认证

<?php

namespace App\Foundation\Repository;

use App;
use Illuminate\Http\Request;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Laravel\Passport\Bridge\UserRepository;
use Laravel\Passport\Bridge\User;
use RuntimeException;

class AdminUserPassportRepository extends UserRepository
{

    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $guard = App::make(Request::class)->get('guard') ?: 'api';//其实关键的就在这里,就是通过传递一个guard参数来告诉它我们是使用api还是admin_api provider来做认证
        $provider = config("auth.guards.{$guard}.provider");
        if (is_null($model = config("auth.providers.{$provider}.model"))) {
            throw new RuntimeException('Unable to determine user model from configuration.');
        }

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

        if (!$user) {
            return;
        } elseif (method_exists($user, 'validateForPassportPasswordGrant')) {
            if (!$user->validateForPassportPasswordGrant($password)) {
                return;
            }
        } elseif (!$this->hasher->check($password, $user->password)) {
            return;
        }

        return new User($user->getAuthIdentifier());
    }
}

登录和单用户系统一样,只是在请求oauth/token的时候带上guard参数,示例代码如下:

Admin/Controllers/Auth/LoginController.php

<?php

namespace Admin\Controllers\Auth;

use Admin\Requests\Auth\LoginRequest;
use App\Http\Controllers\Controller;
use App\Models\AdminUser;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**
     * 获取登录TOKEN
     * @param LoginRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function token(LoginRequest $request)
    {
        $username = $request->get('username');
        $user = User::orWhere('email', $username)->orWhere('phone', $username)->first();

        if ($user && ($user->status == 0)) {
            throw  new UnauthorizedHttpException('', '账号已被禁用');
        }

        $client = new Client();
        try {
            $request = $client->request('POST', request()->root() . '/api/oauth/token', [
                'form_params' => config('passport') + $request->only(array_keys($request->rules())) + ['guard' => 'admin_api']
            ]);
        } catch (RequestException $e) {
            throw  new UnauthorizedHttpException('', '账号验证失败');
        }

        if ($request->getStatusCode() == 401) {
            throw  new UnauthorizedHttpException('', '账号验证失败');
        }
        return response()->json($request->getBody()->getContents());
    }

    /**
     * 退出登录
     */
    public function logout()
    {
        if (\Auth::guard('admin_api')->check()) {
            \Auth::guard('admin_api')->user()->token()->delete();
        }

        return response()->json(['message' => '登出成功', 'status_code' => 200, 'data' => null]);
    }
}

转载请注明: 转载自Ryan是菜鸟 | LNMP技术栈笔记

如果觉得本篇文章对您十分有益,何不 打赏一下

谢谢打赏

本文链接地址: Laravel Passport API 认证使用小结

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 2年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 44
Destiny

:+1:

2年前 评论

看得迷迷糊糊. 的好好看看.

2年前 评论
Ryan

@736713830 实际用的时候就清楚了

2年前 评论

@Ryan 也对,现在用不到.等用到的时候看着文档也瞎弄出来了.....

2年前 评论

多用户情况下,数据库那边不用管吗

2年前 评论
Ryan

@木杉 不用

2年前 评论

Argument 1 passed to App\Providers\AuthServiceProvider::App\Providers{closure}() must be an instance of App\Providers\RouteRegistrar, instance of Laravel\Passport\RouteRegistrar given, called in C:\Users\Administrator\Desktop\laravel\dingo\vendor\laravel\passp
ort\s
rc\Passport.php on line 118 and defined

2年前 评论

dingo已经挂了。。

2年前 评论

结构清晰,代码简单,Good.
话说回来,我一直觉得 password grant 属于 oauth 的异教徒,个人不喜欢这种模式,我个人更喜欢无状态化的 JWT,不需要表去管理了,效率理论上更高(解码速度应该比查库快吧),但鉴权这一块稍微麻烦了点,各有千秋。

2年前 评论
Ryan

@我是谁 我并没发现这个问题,请问哪个文件报错,详细报错信息?

2年前 评论
Mangou

@springjk
赞同 password 属于异教徒。。

通常的jwt规范来说, jwt包含的信息是资源标识符(如主键id)。 解完码还是要查库啊 :smirk:

或者可以丧病下,把权限也包含在jwt里。

不过由于生效失效时间都在颁发时决定了。还是避免不了作废(拉黑)的管理逻辑

2年前 评论

我说一个问题你看这个需求是否满足,前台用户表users,后台用户表admins,同时有一个用户,id都为1,前台id为1的用户登录了,获取到了assess_token,可以正常访问,然后拿着这个token去访问后台,你觉的可以访问么?

2年前 评论
wangwei 4个月前
Ryan

@sowork 请问你按照我上面写的多用户表(多Auth)认证实践过了前台ID的token可以在后台通过验证??

2年前 评论

@Ryan 还没来得及实践,所以我没有说绝对,只是在git上有人讨论过,passport登录以后获取的令牌转换成token,只要是token+id匹配,那么就认为可以访问,理论上这种情况是可以出现的,下面是原文:
The problem after this step is the token saved in oauth_access_tokens only contains the administrator ID. When we use the token received to authenticate, it not look at Administrator models.

2年前 评论
Ryan

@sowork 好的,感谢指出,我找个时间来验证一下

2年前 评论
$request = $client->request('POST', request()->root() . '/api/oauth/token', [
    'form_params' => config('passport') + $request->only(array_keys($request->rules()))
]);

除了这种模拟请求登录的方式 有内部方法可以直接登陆吗?

2年前 评论

请问一下,使用password模式出现这个错误,是怎么回事啊:

Client error: `POST http://xxx/oauth/token` resulted in a `401 Unauthorized` response:\n
{"error":"invalid_credentials","message":"The user credentials were incorrect."}
2年前 评论
Ryan

@justinstar 用户名密码错误,或者OAUTH_CLIENT_ID 和OAUTH_CLIENT_SECRET配置是否正确

2年前 评论

@Ryan 多谢,成功了

2年前 评论

@Ryan 感谢,你的文章对我帮助很大。但有个问题,多用户表认证中,拿 admin user 的 access_token 可以获取 user 信息,该如何解决呢。

2年前 评论

你好,在执行这段代码的时候,

Passport::routes(function (RouteRegistrar $router) {
//对于密码授权的方式只要这几个路由就可以了
config(['auth.guards.api.provider' => 'users']);
$router->forAccessTokens();
});


在文件顶部

use Illuminate\Routing\RouteRegistrar;

抱了这样一个错.

App\Providers\AuthServiceProvider::App\Providers\{closure}() must be an instance of Illuminate\Routing\RouteRegistrar, instance of Laravel\Passport\RouteRegistrar given!

2年前 评论
Ryan

@TYu 换成 use Laravel\Passport\RouteRegistrar; 你导入错了

2年前 评论

不同用户模型的相同ID执行
\Auth::guard('admin_api')->user()->token()->delete();
应该会吧相同ID 的AccessToken 都删掉吧?
比如Admin1和User1任何一个操作应该都会删除oauth_access_tokens表里的user_id为1的数据

2年前 评论

也许用uuid 能解决这个问题

2年前 评论

特意去验证了一下,认证是没有问题的,但是HasApiTokens 这个Trait里的tokens()方法却可以拿到所有与当前用户ID 相同的tokens。

2年前 评论
Ryan

@Jinrenjie 退出登录的时候,是要带着Authorization Header 去调用logout的,\Auth::guard('admin_api')->user()->token()->delete(); 这里的user()->token()->delete() 其实是Authorization Header对于的那个token认证的用户,所以不会把其他用户表中id相同的对应的token delete 从而导致具有相同id的用户退出登录

2年前 评论

你好 按照文档流程 走下来 获取用户信息时候 认证 失败 请问这个怎么解决

file

2年前 评论

file
请问这边是不是应该用AdminUser这个模型

1年前 评论
Ryan

@xuhui 是的,我改下

1年前 评论

@springjk 我看到这个文章就是因为出现混传了能获取到数据...

1年前 评论

将不想暴漏的信息放在passport.php中无效是为什么 5.6

1年前 评论

多用户授权id相同确实有token能共用的问题,我刚验证过了,不知道有没有什么解决方案呢,都不敢用了,,实在不行都只有用原始的jwt自己生成token这种方式了

1年前 评论

@scenery 是不是没有添加到config/app.php的providers配置段

1年前 评论

Argument 1 passed to Laravel\Passport\Bridge\UserRepository::__construct() must be an instance of Illuminate\Hashing\HashManager, instance of Illuminate\Foundation\Application given, called in D:\api\vendor\laravel\framework\src\Illuminate\Foundation\ProviderRepository.php on line 208 多表认证的时候 自定义UserRepository 部分报了一个这样的错 查看源代码 发现 继承的UserRepository这个类中构造函数要求传入一个HashManager $hasher这个参数 该怎样解决啊

1年前 评论

@shea The user credentials were incorrect 请问这个问题怎么解决的啊

1年前 评论

对我很有帮助,感谢!!!,一定要登录上来点赞评论

1年前 评论

不是api认证吗?没有看懂passport拿到token后是如何验证用户的,,,比如那个logout方法,没有看到校验token,那如何验证用户的呢?有点迷糊。。。。。

5个月前 评论
aidybnt 5个月前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!