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 -vvvcomposer 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.phpclass 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,authGET  | oauth/authorize |  web,authPOST | 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\OauthApiTokenMiddlewarenamespace 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,这样会导致他拿到的是 guardauth 的用户,所以重置解析器

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.vuemounted() {    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/