6

Laravel+Passport+Vue实现Oauth2登录认证

 3 years ago
source link: https://surest.cn/archives/165/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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,这样会导致他拿到的是 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.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);
    }
}


本文由 邓尘锋 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Mar 23, 2021 at 10:49 am


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK