Laravel+Passport+Vue 实现 Oauth2 登录认证
前情提要: 这里次要详诉一些细节和实践和局部代码,这里不讲
Oauth2 是什么
阮一峰: OAuth 2.0 的四种形式
这里简略形容一下,Oauth 次要是 4 形式
- 受权码(authorization code)形式,指的是第三方利用先申请一个受权码,而后再用该码获取令牌。
- 隐藏式: 有些 Web 利用是纯前端利用,没有后端, 容许间接向前端颁发令牌。这种形式没有受权码这个两头步骤,所以称为(受权码)” 隐藏式 ”(implicit)。
- 密码式: 如果你高度信赖某个利用,RFC 6749 也容许用户把用户名和明码,间接通知该利用。该利用就应用你的明码,申请令牌,这种形式称为 ” 密码式 ”(password)。
- 凭证式: 最初一种形式是凭证式(client credentials),实用于没有前端的命令行利用,即在命令行下申请令牌。
以上 4 中形式中,除了受权码模式都非常简单,也就是平时的登录 而后 发放 token,而后前端设置 token 申请鉴权即可,这里不说这个,自行实际及了解(我认为和 jwt 的操作没什么区别)
所以次要形容 受权码模式
受权码模式
应用场景
在平时的登录中,应用最多的第一为明码登录,第二的也就是受权码模式这种鉴权形式了
参考: 微信第三方利用应用微信受权登录 、 包含 QQ 受权微博登录 等等
明细
他么都有非常明显的特点,例如微信公众号: 唤起微信用户登录
流程: 公众号登录 -> 微信申请受权 -> 确认 -> 返回原平台 -> 登录胜利
公众号作为一个应用程序,
- 1、获取用户信息的时候,发现用户未登录,而后重定向受权核心(微信),
- 2、微信监测用户登录态,登录态失常(一般来说在公众号中关上都是登录的),用户确认,
- 3、确认后微信依据开发者配置的重定向地址,携带 code 返回公众号。
- 4、公众号监测到用户曾经受权胜利,后端将获取到的 code,向微信发动申请 asses_token
- 5、微信确认 code 可用,返回用户蕴含的权限 (scope) 和 token 曾经 刷新 token
- 6、公众号确认用户信息可用,将 token 存储起来,并且返回数据给客户端
- 7、接下来的每次申请,客户端都将携带 token 向公众号后端发动申请,公众号后端会进行判断 token 是否过期,过期则反复如上步骤
能够参考阮一峰对于受权码模式的形容
在 Laravel 中应用 Passport
举荐官网文档
https://laravel.com/docs/8.x/passport
装置
命令
// 1、请装置对应 Laravel 版本的 passport
// 2、能够疏忽版本 composer require laravel/passport --ignore-platform-reqs -vvv
composer require laravel/passport -vvv
// artisan 运行
// 数据表创立
php artisan migrate
// 秘钥创立
php artisan passport:install
// 创立一个客户端
php artisan passport:client
// 创立实现后,能够从 database.oauth_clients 表中看到
// 留神一下,一般来说你刚创立的 client_id 是 3
// Personal Access Client : 集体拜访客户端模式
// Password Grant Client : 明码拜访模式
模型
// 这里咱们同时配置了 jwt,因为咱们用的是前后端拆散,没有采纳 根本的 web 鉴权登录
namespace App\Models\Module;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class ModuleUsers extends Authenticatable implements JWTSubject
{
protected $table = 't_module_user';
use HasApiTokens, Notifiable;
public function getJWTIdentifier()
{return $this->getKey();
}
public function getJWTCustomClaims()
{return [];
}
}
设置一下自定义的 Client 模型
次要是因为,我想免受权,就是不须要确认受权间接跳转走
namespace App\Models\Passport;
use Laravel\Passport\Client as BaseClient;
class Client extends BaseClient
{
/**
* Determine if the client should skip the authorization prompt.
*
* @return bool
*/
public function skipsAuthorization()
{return true;}
}
-----
// app/Providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider {
...
public function boot()
{$this->registerPolicies();
// 笼罩原来的 model
Passport::useClientModel(Client::class);
}
}
配置 Guard
这里咱们不应用默认的 guard : guard 值得是维持登录态的货色
客户端咱们采纳 client guard
受权核心咱们采纳 oauth guard
oauth 受权核心咱们采纳 jwt
认证形式, 默认的话应用的 laravel 自带的登录
// config/auth.php
'guards' => [
...
'client' => [
'driver' => 'passport',
'provider' => 'moduleUsers',
'hash' => false,
],
'oauth' => [
'driver' => 'jwt',
'provider' => 'moduleUsers',
'hash' => false,
],
]
'providers' => [
...
'moduleUsers' => [
'driver' => 'eloquent',
'model' => App\Models\Module\ModuleUsers::class,
],
]
干掉本来的中间件
首先咱们要确认,哪些路由须要权限认证的哪些不须要的
// 命令
php artisan route:ist
// 后果
// 咱们找到外围的几个路由
办法 | 路由 | 中间件别名
POST | oauth/authorize | web,auth
GET | oauth/authorize | web,auth
POST | oauth/token | throttle
如上所示,咱们须要应用自定义中间件 接管 web 和 auth 路由的鉴权
否则的话根本会弹出 Login 路由不存在
app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
...
// 正文如下路由
// \Illuminate\Session\Middleware\AuthenticateSession::class,
..
]
]
...
protected $routeMiddleware = [
...
// 将原来的路由批改为如下路由
'auth' => \App\Http\Middleware\OauthApiTokenMiddleware::class,
// client 为新增
'client' => ClientApiTokenMiddleware::class,
]
接下来咱们解决一下,auth
中间件,这个中间件的作用次要是 受权核心的认证,也就是如上中说的 微信的作用
// \App\Http\Middleware\OauthApiTokenMiddleware
namespace App\Http\Middleware;
use App\Services\HttpResponse;
use Closure;
use Cyd622\LaravelApi\Response\ApiResponse;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
class OauthApiTokenMiddleware extends BaseMiddleware
{
use ApiResponse;
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{if (strtoupper($request->method()) === "options") {HttpResponse::to();
}
$user = null;
// 获取 cookie,将 cookie 搁置到 header 供 jwt 进行验证
if($token = $request->get('token')) {$user = auth(env('MODULE_LOGIN_GUARD'))->setToken($token)->user();
// 设置解析器,否则 passport 获取不到 module User 用户
$request->setUserResolver(function ($guard = null) {return auth(env('MODULE_LOGIN_GUARD'))->user();});
}
if($user) {return $next($request);
}
HttpResponse::toHttp(401, ['redirect' => \env('MODULE_LOGIN')], '请登录', 200);
}
}
如上看到,咱们会监测其是否领有token
,有的话则接管 $request
申请中的 user,并设置 $request->user()
为咱们对应 guard 的 user
为什么要 setUserResolver
,因为 Passport
默认应用的 是 Auth:user()
的形式来获取user
,这样会导致他拿到的是 guard
为 auth
的用户,所以重置解析器
HttpResponse
间接抛出响应,参考最底部办法详情
当初咱们曾经实现了受权核心的认证了,咱们来编写一下 login 的代码
namespace App\Http\Controllers\Module;
use App\Http\Controllers\Controller;
use App\Http\Requests\Module\LoginRequest;
use App\Models\Module\ModuleUsers;
use App\Traits\AuthTrait;
use Cyd622\LaravelApi\Auth\LoginActionTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class AuthController extends Controller
{
use LoginActionTrait, AuthTrait;
protected $guard = '';
public function __construct()
{$this->guard = "oauth";}
public function login(LoginRequest $request)
{$credentials = request(['account', 'password', 'app_id']);
if (!$token = $this->attempt($credentials)) {return $this->error('账号或者明码谬误', 200, 401);
}
return $this->respondWithToken($token);
}
public function logout()
{auth($this->guard)->logout();
return $this->success('退出登陆胜利');
}
public function refresh()
{return $this->respondWithToken(auth($this->guard)->refresh());
}
protected function respondWithToken($token)
{
return $this->success([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => 60*60*60,
'redirect' => $this->createRedirectUri()]);
}
/**
* 创立受权 uri
*/
protected function createRedirectUri()
{
$query = ['client_id' => request()->get('client_id'),
'redirect_uri' => request()->get('redirect'),
'response_type' => 'code',
'scope' => '',
];
return route('passport.authorizations.authorize', $query);
}
public function attempt($credentials)
{list($username, $password, $app_id) = array_values($credentials);
$account = 'username';
if(filter_var($username, FILTER_VALIDATE_EMAIL)) {$account = 'email';}
$user = ModuleUsers::query()->where($account, $username)->where(compact('app_id'))->first();
if (Arr::get($user, 'password') == $password) {
// 使 guard 登录胜利
$token = auth($this->guard)->login($user);
return $token;
}
return false;
}
}
如上所示,次要看 login
办法和 attempt
办法,登录胜利后,则返回数据,告知前端要进行重定向地址
回到前端
1、创立登录界面,并设置登录
2、创立申请拦截器
这里的登录界面是受权核心的登录界面,所以咱们要晓得登录的客户端是谁,所以咱们携带 client_id 和 redirect_uri 来到登录界面
// login.vue
mounted() {const { redirect_uri, client_id} = this.$route.query;
if (!redirect_uri) {redirect_uri = '/document';}
if (!client_id) {client_id = 3;}
this.client_id = client_id
this.redirect_uri = redirect_uri;
}
// login submit
await login(Object.assign(this.form, {client_id: this.client_id, redirect_uri: this.redirect_uri}))
.then(({data}) => {
const token = data.access_token;
this.$store.dispatch('login', token);
this.loading = false;
this.$notify({
title: '提醒',
message: '登录胜利',
type: 'success'
});
// 登录胜利后,重定向到受权客户端权限的中央,这里因为咱们
// 在 client Model 中 app/Models/Passport/Client.php
// 设置了 skipsAuthorization 办法
// 所以它会间接重定向到,数据库中配置的地址,并且会带上 code
setTimeout(() => {window.location.href = data.redirect + "&token=" + token}, 2000)
})
.catch(() => {this.loading = false;});
前端客户端
async mounted() {
// 设置以后 client id
await this.setClient();
// 查看是否蕴含 code
// code 存在则表明当初登录阶段
await this.hasCode();}
/**
* 设置以后的客户端
*/
setClient() {this.$store.dispatch('client_id', this.client_id);
},
/**
* 监测到蕴含 code 的时候操作
*/
async hasCode() {const { code} = this.$route.query;
if (!code) {return;}
let token = '';
// getToken 对应的
await getToken({code: code, client_id: this.client_id})
.then(({data}) => {
token = data.access_token;
if (!token) {return false;}
})
.catch(e => {console.log(e);
});
await this.$store.dispatch('login', token);
window.location.href = 'document';
},
HttpResponse 办法
仅供参考
<?php
/**
* User: surestdeng
* Date: 2020/5/11
* Time: 15:39:28
*/
namespace App\Services;
use App\Exceptions\DivHttpResponseException as HttpResponseException;
use Illuminate\Http\JsonResponse;
/**
* 抛出一个响应
* User: surestdeng
* Date: 2020/5/11
* Time: 3:45 下午
*/
class HttpResponse
{
/**
* 抛出一个响应
* User: surest
* Date: 2020/5/11
*/
public static function to(int $code = 200, array $data = [], string $message = 'success')
{$response = new JsonResponse(compact('code', 'data', 'message'));
throw new HttpResponseException($response);
}
/**
* 抛出一个特定 httpcode 响应
* User: surest
* Date: 2020/5/11
*/
public static function toHttp(int $code = 200, array $data = [], string $message = 'success', int $httpCode = 200)
{$response = new JsonResponse(compact('code', 'data', 'message'), $httpCode);
throw new HttpResponseException($response);
}
}
原文: https://surest.cn/archives/165/