Laravel passport 多端用户使用
Passport
说明
使用 passport 进行 admin 端和 customer 端的用户认证。
虽然教程很多,但是我并没有参照其他教程完整的走下来,所以记录了自己的开发流程,希望能对其他人有所帮助。
安装项目
laravel new passport
安装 passport
composer require laravel/passport
数据迁移
首先我们需要创建 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
执行迁移
php artisan migrate --seed
passport 初始化
php artisan passport:install
此时在 storage 下会生成 oauth-private.key 和 oauth-public.key
生成认证
目前我们只是为前后端分离的后台使用,所以 password 模式足够
php artisan passport:client --password --name='passport-admin'
php artisan passport:client --password --name='passport-customer'
备注
原先我以为这里采用不一样的数据之后下面 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
增加如下代码
因为 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
分别修改为如下代码
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
安装 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 和参数应该能明白
ok,按照预期的该返回的返回,不通过的也没通过
接下来我们用这些 token 获取用户信息
图 5 为我用 admin1 的 token 请求 customer 的接口
图 6 为我用 customer1 的 token 请求 admin 的接口
都是无效的。
接下来验证刷新 token
admin1 的 refresh_token
再请求一下
customer1 的 refersh_token
ok, 完工!
总结
-
虽然 refresh_token 不能重新刷出来,但是之前没过期的 access_token 其实依然会有效
-
个人觉得这个并不如 dingoapi + jwt (也就是第三本 api 的教程)好用,本文只说明怎么使用 passport 进行多端验证。像抛出异常,没有自定义返回码等还有一大堆未完善的东西。
参考资料
博客:Laravel5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 重点感谢
博客:Laravel Passport API 认证使用小结
博客:Laravel 5.5 使用 Passport 实现 Auth 认证
本作品采用《CC 协议》,转载必须注明作者和本文链接
内容挺好的,刚好是我需要的,要是能把文章格式调一下就好了 :+1:
用laravel 8.x认证报错为:
Client error:
POST http://laravel-passport-oauth.loc/admin/oauth/token
resulted in a400 Bad Request
response: {"error":"invalid_grant","error_description":"The user credentials were incorrect.","message":"The user credentials were (truncated...)