使用laravel/passport实现Oauth2登录,并结合到Laravel-admin
环境介绍
- 开发工具: phpstorm
- 运行环境:
- Homestead 9.0(Ubuntu18.04)
- php7.2
- mysql 5.7
- larave 5.5.45
- passport 4.0.3
- laravel-admin 1.7.2
业务场景
实际业务中需要多个后台,后台是基于Laravel-admin
构建的,主后台中存放这业务数据,其余的分后台需要从主后台获取管理员数据.
具体流程
- 首选主后台管理员登录
- 管理员点击进入分后台时系统生成授权码
- 管理员携带授权码登录到分后台
- 分后台根据授权码获取该用户的
access_token
和refresh_token
- 获取成功以后进行其他相关操作
开发流程
安装Passport
, Laravel-admin
composer require laravel/passport
composer require encore/laravel-admin
......
注意 Laravel 5.5
需要使用 laravel/passport 4.0
分析Passport
中的实现逻辑,在 主系统(QQ) 中实现OAuthServer
首先我们查看一下路由
oauth/authorize | \Laravel\Passport\Http\Controllers\[email protected]
oauth/authorize | \Laravel\Passport\Http\Controllers\[email protected]
oauth/authorize | \Laravel\Passport\Http\Controllers\[email protected]
oauth/clients | \Laravel\Passport\Http\Controllers\[email protected]
oauth/clients | \Laravel\Passport\Http\Controllers\[email protected]
oauth/clients/{client_id} | \Laravel\Passport\Http\Controllers\[email protected]
oauth/clients/{client_id} | \Laravel\Passport\Http\Controllers\[email protected]
oauth/personal-access-tokens | \Laravel\Passport\Http\Controllers\[email protected]
oauth/personal-access-tokens | \Laravel\Passport\Http\Controllers\PersonalAccessTok[email protected]
oauth/personal-access-tokens/{token_id} | \Laravel\Passport\Http\Controllers\[email protected]
oauth/scopes | \Laravel\Passport\Http\Controllers\[email protected]
oauth/token | \Laravel\Passport\Http\Controllers\[email protected]
oauth/token/refresh | \Laravel\Passport\Http\Controllers\[email protected]
oauth/tokens | \Laravel\Passport\Http\Controllers\[email protected]
oauth/tokens/{token_id} | \Laravel\Passport\Http\Controllers\[email protected]
分析前三个路由 oauth/authorize
,分别对应的是显示授权页面,拒绝授权,接受授权
嵌入接收授权页面到 Laravel-admin
使用命令发布passport的视图文件
php artisan vendor:publish --tag=passport-views
授权页面在 resources/views/vendor/passport/authorize.blade.php
,这个页面可以自己设计,也可以直接参考 Passport
自带的授权页面,但是注意其中的表单提交路径!
增加一个直接进入授权页面的控制器
为了方便理解,我这边将我们的 主后台 称为 QQ ,分后台 称为 百度
一般的授权流程是由用户在第三方系统发起的,比如用户在百度上选择QQ登录,然后跳转到QQ进行授权,而我们的系统需要直接向用户展示QQ登录页面,然后直接重定向到百度.
- 正常流程
百度->选择QQ登录->QQ->用户授权->302到百度->百度根据code获取用户信息
- 我们需要的流程
QQ->用户授权->302到百度->百度根据code获取用户信息
控制器实际上就是一个发起跳转,来代替用户在百度上选择QQ登录这一操作.注意此处 authorize.get
是我自定义的路由别名,请根据实际情况修改代码.
$url=route('authorize.get',[
'client_id' => $this->mode->client_id,
'redirect_uri' => $this->mode->admin_url,
'response_type' => 'code',
'scope' => '',
]);
redirect($url);
自定义授权路由
通过观察源码发现系统路由存在一个坑点,Admin后台的用户登录部分判断使用的都是 Admin::guard()
方式去获取 guard
,而Passport是使用 $request->user()
方式获取的,这样获取的就是默认的guard,默认的guard在config/auth.php
中可以配置.既然访问的路径需要修改,直接自己创建路由操作起来比较方便.
admin中间件对应的代码位于vendor/encore/laravel-admin/src/Middleware/Authenticate.php
......
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$redirectTo = admin_base_path(config('admin.auth.redirect_to', 'auth/login'));
if (Admin::guard()->guest() && !$this->shouldPassThrough($request)) {
return redirect()->guest($redirectTo);
}
return $next($request);
}
......
此处Admin
是Facade
,找到真正的实现位于 vendor/encore/laravel-admin/src/Admin.php
其中获取 guard
方法如下:
/**
* Attempt to get the guard from the local cache.
*
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard()
{
$guard = config('admin.auth.guard') ?: 'admin';
return Auth::guard($guard);
}
所以我定义了两个路由用户显示授权页面以及处理授权请求.
$router->group(['prefix'=>'oauth','middleware'=>'auth:'.(config('admin.auth.guard') ?: 'admin')],function (Router $router){
$router->get('authorize','\Laravel\Passport\Http\Controllers\[email protected]')->name('authorize.get');
$router->post('authorize','\Laravel\Passport\Http\Controllers\[email protected]');
});
自定义Token的获取路由
Route::post('/admin/api/oauth/token','\Laravel\Passport\Http\Controllers\[email protected]')->middleware('throttle');
在分后台访问上面的路由就可以获取到用户的 access_token
和 refresh_token
,这里通过curl来模拟
curl -X POST \
http://homestead.test/admin/api/oauth/token \
-F grant_type=authorization_code \
-F client_id=1 \
-F client_secret=Kkwcww*****Qxy0uJIp4D \
-F redirect_uri=http://*****.test/admin \
-F code=def502009c********2cf590257f7ff704d7c5
响应如下
{
"token_type": "Bearer",
"expires_in": 31622400,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI**************Bw6n6bZX6qC0pgqzLpXV5yRC2B5R0",
"refresh_token": "def502009b328a313c3c414c6eca26**********f855c7da9625f9"
}
以上完成了显示授权页面,生成授权 code
,获取 access_token
和 refresh_token
.还需要提供一个获取用户信息的接口
使用 access_token
获取用户基本信息
这个过程相对会比较复杂一点,我分X步来完成.
- 新增
Guard
用于 api认证Guard译为看守,相当于说是用来控制用户登录的,一个guards数组里面都会有两个元素
driver
和provider
,provider
一般是提供认证方法的,driver
就是这个认证流程的驱动,通过session
,token
,passport
等不同的驱动来维护登录状态.所以我们需要定义一个新的Guard用来判断分系统的API通讯认证,修改laravel-admin的配置文件config/admin.php
,新增admin_api
Guard,驱动选择passport
,这里的admin_api
就是我新加的....... 'auth' => [ ...... 'guards' => [ 'admin' => [ 'driver' => 'session', 'provider' => 'admin', ], 'admin_api' => [ //新增的Guard 'driver' => 'passport', 'provider' => 'admin' ] ], ....... ], ......
- 修改模型
需要在模型中添加
use HasApiTokens;
,因为passport需要一些关联关系来获取用户认证时需要的信息.这边需要继承原有的模型,增加use....... use \Encore\Admin\Auth\Database\Administrator; use Laravel\Passport\HasApiTokens; class Admin extends Administrator { use HasApiTokens; ...... }
修改laravel-admin的配置信息,启用自定义的用户模型.
...... 'providers' => [ 'admin' => [ 'driver' => 'admin', 'model' => Admin::class, //必改 ], ], ...... 'users_model' => Admin::class, //可以选择不改 ......
- 新增一个控制器用来获取用户信息
这边要注意一下路由定义的位置,因为laravel-admin的路由组是带admin的中间件的,这个中间件是通过
admin
Guard来判断用户是否登录的,如果定义在一起会被拦截下来.(如果需要继续使用admin的权限管理需要自己引入对应中间件,会涉及一些其他问题,比如默认的Guard需要调整,我这边就不引入了)Route::group([ 'prefix' => config('admin.route.prefix').'/api', 'middleware' => 'auth:admin_api', 'namespace' => config('admin.route.namespace'), ], function (Router $router) { $router->get('admin_user_info','[email protected]'); });
laravel-admin中注册的路由: (文件位于
vendor/encore/laravel-admin/src/AdminServiceProvider.php
)...... protected $routeMiddleware = [ 'admin.auth' => Middleware\Authenticate::class, 'admin.pjax' => Middleware\Pjax::class, 'admin.log' => Middleware\LogOperation::class, 'admin.permission' => Middleware\Permission::class, 'admin.bootstrap' => Middleware\Bootstrap::class, 'admin.session' => Middleware\Session::class, ]; ......
修改 分系统(百度) 的登录逻辑
由于系统自带的登录逻辑是通过用户名+密码的形式,而我们这边采用的只有一个code字段,了解Laravel登录逻辑的就会发现登录分为 retrieveByCredentials
和 validateCredentials
这两步,至于其他的 retrieveByToken
和 updateRememberToken
在我们这种情景下是不适用的.所以直接覆盖掉原有的函数即可.接下来的重点是 retrieveByCredentials
和 validateCredentials
的实现了.
定义新的 Provider
并注册
创建 AdminAuthServiceProvider
然后在 app/Providers/AuthServiceProvider.php
中注册别名为 admin
的provider
namespace App\Providers;
use GuzzleHttp\Client;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\EloquentUserProvider;
class AdminAuthServiceProvider extends EloquentUserProvider
{
public function retrieveByToken($identifier, $token)
{
}
public function updateRememberToken(Authenticatable $user, $token)
{
}
public function retrieveByCredentials(array $credentials)
{
$http = new Client();
$response = $http->post(env('OAUTH_SERVER') . '/admin/api/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => env('CLIENT_ID'),
'client_secret' => env('CLIENT_SECRET'),
'redirect_uri' => env('APP_URL') . '/admin/auth/login',
'code' => $credentials['code'],
],
]);
$oauthToekn = json_decode((string)$response->getBody(), true);
$response = $http->get(env('OAUTH_SERVER') . '/admin/api/admin_user_info', [
'headers' => ['Authorization' => 'Bearer ' . $oauthToekn['access_token']],
]);
$user = json_decode((string)$response->getBody(), true)['data'];
$user = Admin::firstOrCreate($user);
$user->access_token = $oauthToekn['access_token'];
$user->refresh_token = $oauthToekn['refresh_token'];
$user->token_type = $oauthToekn['token_type'];
$user->save();
return $user;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
return !empty($user->access_token) && !empty($user->refresh_token);
}
}
public function boot()
{
......
Auth::provider('admin', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new AdminAuthServiceProvider($app['hash'], $config['model']);
});
......
}
修改 config/admin.php
中的 auth.providers.admin.driver
,改为admin
......
'providers' => [
'admin' => [
'driver' => 'admin',//修改此处为上面定义的admin
'model' => Encore\Admin\Auth\Database\Administrator::class,
],
],
......
覆盖 AuthController
中的 getLogin
和 postLogin
class AuthController extends BaseAuthController
{
public function getLogin()
{
//验证是否登录
if ($this->guard()->check()) {
return redirect($this->redirectPath());
}
$credentials = request()->only(['code']);
if ($this->guard()->attempt($credentials)) {
return $this->sendLoginResponse(request());
}
abort(405);//登录失败抛出异常,根据具体情况修改
}
public function postLogin(Request $request)
{
abort(405);//屏蔽默认的postLogin方法,可以随便抛出一个异常,个人认为这样做比较方便也没有什么隐患
}
}