• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • laravel9 + vue3 后端使用 passport 来生成验证 token

    在 Laravel 9 中后端支持用户认证,生成 token,有三类实现办法:

    传统认证
    认证套件:Laravel JetstreamLaravel BreezeLaravel Fortify。他们的都能自动生成:访问路由、访问控制器类、视图文件。适用于传统 MVC 编程方式php+blade 模板。基于cookie+session身份验证服务。
    中文网址:https://learnku.com/docs/laravel/9.x/authentication/12239

    Sanctum 认证
    Sanctum 使用 Laravel 内置的基于cookie+session身份验证服务,默认已经安装配置。为 SPA(单页面应用程序)、移动应用程序和简单的、基于访问令牌token的 API 提供轻量级身份验证系统。
    中文网址:https://learnku.com/docs/laravel/9.x/sanctum/12272

    Passport 认证
    采用oauth2.0认证方式。适用于前后端分离模式 API移动应用程序 API。使用token认证。其使用说明中有前端vue代码示例,以及axios网络请求示例。Passport 可以在几分钟之内为你的应用程序提供完整的 OAuth2 服务端实现。
    中文网址:https://learnku.com/docs/laravel/9.x/passport/12270

    OAuth 是一个开发授权标准,允许通过授权的方式让第三方应用访问该用户在某一网站上存储的需要认证的资源,而无需将用户名和密码提供给第三方应用。在前后端分离的 API 认证中,我们可以把前端应用看作第三方应用,后端应用自然就是这里的网站了,用户认证信息存储在后端网站,当前端需要访问认证资源时,通过后端应用授权的方式访问(授权的前提是前端应用在后端应用注册过,否则不能授权),只有用户允许授权,才可以访问认证资源,否则还是不能访问。


    Passport 支持五种认证模式的安装配置方式:
    token 类型认证模式使用场景
    Password Grant授权码授权令牌通过 PKCE(Proof Key for Code Exchange,中文译为”代码交换的证明密钥”)发放授权码,是对单页面应用或原生应用进行认证,以便访问 API 接口的安全方式。这种颁发方式的适用场景是当你不能保证客户端密钥被安全存储,或者为了降低攻击者拦截授权码的威胁。在这种模式下,当通过授权码获取访问令牌 token时,用“验证码”(code verifier)和“质疑码”(code challenge,“challenge”,名词可译为‘挑战;异议;质疑’等)的组合来交换客户端密钥。
    密码授权令牌OAuth2 密码授权机制可以让你自己的客户端(又称第一方客户端),例如移动应用,使用用户名和密码(或邮箱地址+密码)获取访问令牌 token。这使得你可以安全地向自己的客户端发出访问令牌 token,而不必走整个 OAuth2 授权代码重定向流程。
    隐式授权令牌隐式授权和授权码授权有点相似,不过,无需获取授权码,访问令牌 token就会返回给客户端。通常适用于同一个公司自有系统之间的认证,尤其是客户端应用不能安全存储令牌信息的时候。无法安全存储客户端凭据的 javascript 或移动应用。
    客户端授权令牌客户端凭证授予访问令牌 token适用于计算机到计算机的身份验证。不涉及到用户的互动。例如,你可以在调度任务中,使用这种授权来通过 API 执行维护任务。
    Personal Access私人访问令牌有时候,你的用户可能想要颁发访问令牌 token给自己而不走典型的授权码重定向流程。允许用户通过应用程序用户界面给自己发出访问令牌 token,有助于用户体验你的 API,或者也可以将其作为一种更简单的发布访问令牌 token的方式。



    需求实现:

    • 此前后端分离的项目,前后端都有各自的独立域名(www.example.comadmin.example.comapi.example.com)、独立的程序语言代码(前端 vue,后端 PHP)。还可以分别部署在不同的物理服务器上。
    • 前端内容浏览支持 PC、移动端、还可以扩展为支持微信小程序调用、支持 app 调用
    • 前端前台,无需登录,即可浏览展示内容;前端前台,用户中心登录,进行个人内容管理。前端后台管理,需要登录管理。

    综上所述,需要三个 token 访问令牌。前端前台展示内容,客户端凭证令牌access_token;前端前台用户中心,密码授权令牌user_token;前端后台登录管理,密码授权令牌admin_token



    安装 Passport

    Laravel 9 安装配置 Passport,官方文档

    第一步:下载 Passport

    cd /var/web/www/exampleApi
    
    composer require laravel/passport
    

    contOS 操作系统下,查看变更和新增加的目录文件

    cd /var/web/www/exampleApi
    ls -lt --time-style=long-iso ./
    
    //显示如下:
    total 54044
    drwxr-xr-x 52 root root     4096 2022-05-31 20:33 vendor
    -rw-r--r--  1 root root   332236 2022-05-31 20:33 composer.lock
    -rw-r--r--  1 root root     1776 2022-05-31 20:33 composer.lock
    ......
    

    参数-lt分别表示:

    • l:表示使用长列表格式。
    • t:表示按照文件的修改时间。
    • r:表示逆序显示内容。

    参数--time-style=long-iso表示,设置列表中的显示时间样式,使用long-iso样式。

    被安装目录vendor/laravel/passport版本:passport 11.0


    composer.json 内容:
    
    "require": {
        "php": "^8.0.2",
        "guzzlehttp/guzzle": "^7.2",
        "laravel/framework": "^9.19",
        "laravel/passport": "^11.0",
        "laravel/sanctum": "^3.0",
        "laravel/tinker": "^2.7"
    },
    

    查看已经安装的拓展包,在根目录下执行 Linux 命令:

    composer show -i
    
    ......
    guzzlehttp/guzzle                  7.5.0     Guzzle is a PHP HTTP client library
    guzzlehttp/promises                1.5.2     Guzzle promises library
    guzzlehttp/psr7                    2.4.1     PSR-7 message implementation that also provides common utility methods
    hamcrest/hamcrest-php              v2.0.1    This is the PHP port of Hamcrest Matchers
    laravel/framework                  v9.26.1   The Laravel Framework.
    laravel/passport                   v11.0.0   Laravel Passport provides OAuth2 server support to Laravel.
    laravel/pint                       v1.1.1    An opinionated code formatter for PHP.
    laravel/sail                       v1.15.4   Docker files for running a basic Laravel application.
    laravel/sanctum                    v3.0.1    Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.
    laravel/serializable-closure       v1.2.0    Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.
    laravel/tinker                     v2.7.2    Powerful REPL for the Laravel framework.
    ......
    


    第二步:运行 Passport 的迁移命令

    Passport 服务提供器使用框架注册自己的数据库迁移目录,因此在注册提供器后,就应该运行 Passport 的迁移命令来自动创建存储客户端和令牌的数据表:

    php artisan migrate
    

    显示如下:
    INFO Preparing database.
    Creating migration table ............................................... 131ms DONE

    INFO Running migrations.
    2016_06_01_000001_create_oauth_auth_codes_table ........................ 605ms DONE
    2016_06_01_000002_create_oauth_access_tokens_table ..................... 459ms DONE
    2016_06_01_000003_create_oauth_refresh_tokens_table .................... 303ms DONE
    2016_06_01_000004_create_oauth_clients_table ........................... 176ms DONE
    2016_06_01_000005_create_oauth_personal_access_clients_table ........... 120ms DONE
    2019_12_14_000001_create_personal_access_tokens_table .................. 340ms DONE

    执行上面命令后,会在数据库中生成五个数据表:

    • db_oauth_access_tokens:通过认证token表。
    • db_oauth_auth_codes:认证code表。
    • db_oauth_clients:认证客户端。
    • db_oauth_refresh_tokens:刷新token表。
    • db_oauth_personal_access_clients:个人授权客户端。
    • db_personal_access_tokens:Laravel 中内置了 santum,它是专门用来 api 认证生成 token 的扩展包,不过需要自己配置才能使用。这个表就是 sanctum 定义的,存放用户对应的 token 相关字段信息。


    第三步:生成客户端和密钥

    创建生成安全访问,「personal access」客户端 client id、密钥 client secret令牌。同时,也会创建用于生成,「password grant」客户端 client id、密钥 client secret:

    php artisan passport:install
    

    类型数据适用
    personal access客户端 Client ID: 1
    加密密钥 Client secret: aJ9qYvzKMY3h5rG14gG5iWo2edcDn2vfaYJxLrBi
    私人模式
    password grant客户端 Client ID: 2
    加密密钥 Client secret: ILk55sc8swghKbvl4qfU4to3aW1gMbZq1z6lyKpL
    • 授权码授权模式
    • 账号密码授权模式
    • 隐式授权模式
    • 客户端授权模式
    • 上面的命令,会在storage目录下生成oauth-private.keyoauth-public.key,分别包含 OAuth 服务的私钥和公钥,用于安全令牌的加密解密。
    • 上面的命令,会在db_oauth_clients数据表中生成两条记录,相当于注册了两个客户端应用,一个用于密码授权令牌认证,一个用于私人访问令牌认证。db_oauth_personal_access_clients有一条记录。至此其他 token 数据表中仍然无数据。

    ⚠️注意:上传到服务器生成环境中,为了密钥安全,需要重新生成一次密钥。密钥文件storage/oauth-private.keystorage/oauth-public.key,可以拥有777权限(-rwxrwxrwx),也可以是644权限(-rw-r--r--)。

    php artisan passport:keys  --force
    


    登录 mysql 数据库,查看数据表内容:

    select * from db_oauth_personal_access_clients\G
    
    *************************** 1. row ***************************
            id: 1
     client_id: 1
    created_at: xxxxxx
    updated_at: xxxxxx
    
    select * from db_oauth_clients\G
    
    *************************** 1. row ***************************
                        id: 1
                   user_id: NULL
                      name: Laravel Personal Access Client
                    secret: aJ9qYvzKMY3h5rG14gG5iWo2edcDn2vfaYJxLrBi
                  provider: NULL
                  redirect: http://localhost
    personal_access_client: 1
           password_client: 0
                   revoked: 0
                created_at: xxxxxx
                updated_at: xxxxxx
    *************************** 2. row ***************************
                        id: 2
                   user_id: NULL
                      name: Laravel Password Grant Client
                    secret: ILk55sc8swghKbvl4qfU4to3aW1gMbZq1z6lyKpL
                  provider: users
                  redirect: http://localhost
    personal_access_client: 0
           password_client: 1
                   revoked: 0
                created_at: xxxxxx
                updated_at: xxxxxx
    


    Laravel 框架生命周期

    1. 首先,所有的请求request都是经由入口public/index.php文件,加载Composer生成的自动加载设置。然后,进入bootstrap/app.php,创建 Laravel 应用程序的实例。然后启动创建一个应用程序/服务容器。
    2. 然后,所有的请求request被发送到 HTTP 内核(用于处理 Web 请求)或控制台内核(用于处理Artisan 命令)。HTTP 内核文件app/Http/Kernel.php,继承了Illuminate\Foundation\Http\Kernel

      HTTP 内核启动【配置】中的程序。配置在bootstrappers数组内,包括错误处理,日志,检测应用环境,以及其它在请求被处理前需要执行的任务。
      HTTP 内核注册启动【服务】。配置在config/app.php中。服务提供者给予框架开启多种多样的组件,像数据库,队列,验证器,以及路由组件。只要被启动服务提供者就可支配框架的所有功能。默认的服务存放在app/Providers下。
      HTTP 内核册启动【HTTP中间件】。配置在app/Http/Kernel.php中。这些中间件处理 HTTP 会话、读写 HTTP session、判断应用是否处于维护模式、验证 CSRF 令牌等等。所有的这些中间件都位于app/Http/Middleware目录。

    3. 然后,所有的请求request将会被交给路由器进行分发。配置在routes目录下。路由将会调度请求,先执行路由绑定的 HTTP中间件,后执行路由绑定的控制器,控制器位于app/Http/Controllers下。



    客户端凭证令牌

    第四步:生成客户端令牌

    客户端凭证令牌。这种授权方式不需要走典型的登录或授权重定向流程,适用于机器与机器之间的接口认证。

    php artisan passport:client --client
    

    此命令在db_oauth_clients数据表又生成一条记录。其他四个表,无新增数据。

     select * from db_oauth_clients\G
    
    
    *************************** 3. row ***************************
                        id: 3
                   user_id: NULL
                      name: accessToken
                    secret: wE8I3TehjA9Xxkrubu9miUUnFlMMfljsMA9lJL5p
                  provider: NULL
                  redirect:
    personal_access_client: 0
           password_client: 0
                   revoked: 0
                created_at: xxxxxx
                updated_at: xxxxxx
    


    第五步:添加验证中间件

    passport 内置提供了客户端 token 验证的中间件CheckClientCredentials。要使用这种授权,需要在app/Http/Kernel.php$routeMiddleware属性中添加。若全部站点内容,只使用这一个 token ,可以直接添加到$middlewareGroups属性中。由于业务需求,分别控制路由,所以,这里添加到$routeMiddleware

    protected $routeMiddleware = [
        'accessToken'=> \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
      ];
    


    第六步:添加路由控制

    /routes/api.php路由上,添加中间件,控制多个访问路径,使访问必须经过此 token 令牌认证。登录 login、验证路由 oauth 等,不受其验证控制。

    <?php
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\Api;
    
    
    //测试
    Route::get('/', function () {
        return 'ok';
    });
    
    Route::prefix('home')->group(function () {
        Route::post('/token/client',[Api\TokenController::class,'client']);
        Route::post('/token/login',[Api\TokenController::class,'userLogin']);
        Route::middleware('accesssToken')->group(function () {
            Route::get('/topic/list',[Api\TopicController::class,'topicList']);
            Route::get('/novel/list',[Api\NovelController::class,'novelList']);    
        });
    });
    

    Laravel 框架默认设置,访问路径中拥有api前缀的,才会启用/routes/api.php路由。根据需求,又增加了home前缀,用于前端前台访问路径,admin前缀,用于前端后台访问路径。

    • https://api.example.com/api,用于测试。
    • https://api.example.com/api/home/token/client,前端前台获取 token。
    • https://api.example.com/api/home/token/login,前端前台 user 登录。
    • https://api.example.com/api/home/topic/list,前端前台文章列表。
    • https://api.example.com/api/home/novel/list,前端前台书籍列表。


    第七步:获取令牌

    <?php
    namespace App\Http\Controllers\Api;
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Http;
    use App\Http\Controllers\Controller;
    use App\Models\User;
    
    class TokenController extends Controller
    {
        public function client()
        {
            $tokenObj=DB::table('oauth_clients')->where('id','=',3)->first();
            $response = Http::asForm()->post('https://api.example.com/oauth/token', [
                    'grant_type' => 'client_credentials',
                    'client_id' =>  3,
                    'client_secret' =>  $tokenObj->secret,
                    'scope' => '*',
            ]);
            
           return response()->json($response->json());
        }
    }
    

    https://api.example.com/api/home/token/client发送请求,返回 token 内容如下:

    {
      "token_type":"Bearer",
      "expires_in":31536000,
      "access_token":"eyJ0eXAiOiJKVlQiLCJhbGciOiJSUzIlNiIsImp0aSI6IjQzMjAyYmY2YzRkNTMzOTg0NWI5MzhlOTFlY2Q5NzUzNmRIMzkxZmFhYzYlNzI5NTdjYTIzM 
    DMwNWIlYTQwYTI5ZjNiMTE0ZTAzYzVmYWIxIn0.eyJhdWQiOiIyIiwianRpIjoiNDMyMDJiZjZjNGQlMzM5ODQlYjkzOGU5MWVjZDk3NTM2ZGUzOTFmYWF 
    jNjU3MjklN2NhMjMwMzAlYjVhNDBhMjlmM2IxMTRlMDNjNWZhYjEiLCJpYXQiOjElNDIzNjclNzQsIm5iZiI6MTU0MjM2NzU3NCwiZXhwIjoxNTczOTAzN 
    Tc0LCJzdWIiOiJwTnkzcWtENlFxN3d4Yk9ydm9ueiIsInNjb3BlcyI6WyIqII19.dAClgVaTbZTlXbR8LBIDNaHy94JluPIJVmDtM8SKDS0vJ7au7nXClI 
    M9qTpIu3_n_wr2YdQE17Pxy_wO0vL4ZcbjJKkCntJMJGJj23vjSRT0uE4H3yqG5WqRrB5hklNHHKgONLAR873h9Ou2U189DKXAtpe4ZfMKWg2EvgorX762 
    mvQOSC3TlGcPL2VoE0Bi4Sie8hGY0D8fk4EUfanfGJf_XZMSQaqcCfqc0uiwWBCNDqHIQ_VyzE3spdU40yBVKJjZxkq3GHMSwQjRmOlvuiqUkcVaEqmvz 
    ur0z0XlkM9vXam7H4BND24OR79oypHv3rIliYU3hZO8d9INr2mtIQFB9_BP7N_W5M6EgdlTNYqypIhlB35hxW9B66HfzIORckQpQrAvOO6T5wgisl9G2Lo 
    CkiZy rA0qQxn__rw_00Yu2Kjagrk3HzNrsDpU2YHqniuNUIRfHenFQboFsOWflesfWe3MOX2O9Jstpe48c2_b7JW35hH0-2wARkPUoxBn6y_7whVRe_wsgZ2Y9X-HlhKjympZiP9yb rxA30FVD9j pZoEvehAcAD_Y6iTnB7dCuXvfrL86MKdqZmoIqxFbAQ4fIVm4oY3A8zxTlQtBWm2T9pprXFqtl4Ss548_Aj22ikxenLpKT3C_NsSi_n7jK7uy8EbmdTkwaPe6WeTI"
    }
    

    此类型的 token 不刷新,有效期默认为一年。没有返回用于刷新令牌的 refresh_token 字段。客户端每次请求得到的 token 值是不同的,但都是默认一年的有效期。在前端 axios 请求 header 头中,Authentication:Beareraccess_token,携带 token 请求访问 API 数据。前端每次请求 token,每次会得到一个不同的 token 值,同时,还会在数据表db_oauth_access_tokens,生成一条记录,用于 token 验证。

    若要改变有效期限,需要在app/Providers/AuthServiceProvider.php文件boot方法中,进行修改。

    ⚠️注意:Passport 11.0+,不存在Passport::routes()方法,由 Passport 自动注册 oauth/token 路由。Passport 11.0以前的版本,在app/Providers/AuthServiceProvider.php文件boot方法中,需要手动添加Passport::routes(),来注册路由。


    Guzzle是一个 PHP HTTP 客户端,致力于让发送 HTTP 请求以及与 Web 服务进行交互变得简单。Laravel 为Guzzle HTTP 客户端提供了一套语义化且轻量的 API,让你可用快速地使用 HTTP 请求与其他 Web 应用进行通信。该 API 专注于其在常见用例中的快速实现以及良好的开发者体验。在开始之前,你需要确保你的项目已经安装了 Guzzle 包作为依赖项。默认情况下,Laravel 已经包含了 Guzzle 包。查看根目录下composer.lock文件。文档资料:HTTP 客户端



    密码授权令牌

    OAuth2 的密码授权方式允许你自己的客户端(比如手机端应用),通过使用邮箱/用户名和密码获取访问秘钥。这样你就可以安全的为自己发放令牌,而不需要完整地走 OAuth2 的重定向授权访问流程。参考文档:密码授权方式的令牌

    路由守卫
    Laravel框架的Auth认证组件由「guards」和「providers」组成,Guard 定义了用户在每个请求中如何实现认证,Provider 定义了如何从持久化存储中获取用户信息。配置文件位于config/auth.php


    第八步:配置登录用户路由守卫

        'guards' => [
            'web' => [
                'driver' => 'session',
                'provider' => 'users',
            ],
     
            'userApi' => [
                'driver' => 'passport',
                'provider' => 'user',
            ],
    	'adminApi' => [
                'driver' => 'passport',
                'provider' => 'admin',
            ],
        ],
    
        'providers' => [
            'users' => [
                'driver' => 'eloquent',
                'model' => App\Models\User::class,
                'hash' => true,
            ],
    	'user' => [
                'driver' => 'eloquent',
                'model' => App\Models\User::class,
                'hash' => true,
            ],
    	'admin' => [
                'driver' => 'eloquent',
                'model' => App\Models\Admin::class,
                'hash' => true,
            ],
        ],
    

    守卫采用passport,有App\Models\User::classApp\Models\Admin::class来核实用户身份信息。


    第九步:给数据模型增加辅助函数

    文件路径vendor/laravel/passport/src/Passport.php,其命名空间Laravel\Passport\HasApiTokens,把它添加到数据模型中,模型会使用HasApiTokens中的属性和方法。这个Trait会给你的模型提供一些辅助函数,用于检查已认证用户的令牌和使用范围。可查阅文件路径vendor/laravel/passport/src/HasApiTokens.php

    User 模型

    <?php
    namespace App\Models;
    
    use Illuminate\Foundation\Auth\User as Authenticatable;
    use Illuminate\Notifications\Notifiable;
    use Illuminate\Support\Facades\Hash;
    use Laravel\Passport\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;
    
        //与模型关联的数据表,不需要填写前缀。注意:此处是 user 数据表,非 Laravel 自带的 users 数据表。
        protected $table = 'user';
    
        //与数据表关联的主键
        protected $primaryKey = 'uid';
    
        //模型的属性默认值
        protected $attributes = [
            'yn' => 1,
        ];
    
        //为序列化而应隐藏的属性
        protected $hidden = [
            'password','remember_token',
        ];
    
        //通过Passport的密码授权验证用户使用的密码
        public function validateForPassportPasswordGrant($password)
        {
        	//注册账号的时候,密码经过 Hash 加密,所以检验的时候,密码需要经过 Hash 验证
            return Hash::check($password, $this->password);
        }
    }
    

    注意:此处是 user 数据表,非 Laravel 自带的 users 数据表。模型中,不能拥有public $timestamps = false;。否则,不能获得 token。因为数据表是ON UPDATE CURRENT_TIMESTAMP是由 mysql 自动维护的。

    user 模型介绍。参考: Eloquent:入门

    • use Illuminate\Notifications\Notifiable;是消息通知相关功能引用。参考文档:消息通知
    • use Illuminate\Foundation\Auth\User as Authenticatable;是授权相关功能的引用。Auth认证功能以及$request->validate验证器。参考文档:用户认证
    • use Illuminate\Contracts\Auth\MustVerifyEmail;邮箱验证契约。发送和校验电子邮件的验证请求。参考文档:Email 认证。可与use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;Trait配合使用。
    • use Illuminate\Support\Facades\Hash;当用户的 password 使用 Hash 加密过的时候,登录的时候,需要再次使用 Hash 来验证。Hash::check
    • use Illuminate\Database\Eloquent\Factories\HasFactory;本机测试的时候,批量添加数据的工厂模式,上传到服务器生产环境下,需要去掉。


    user数据表。非 Laravel 默认的 users 数据表。

    DROP TABLE IF EXISTS `db_user`;
    CREATE TABLE IF NOT EXISTS `db_user` (
      `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `mobile` char(11) DEFAULT NULL,
      `email` varchar(30) DEFAULT NULL,
      `password`  VARCHAR(255)  DEFAULT NULL,
      `nickname` varchar(20)  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
      `icon` varchar(200) DEFAULT NULL,
      `qq_openid` varchar(64) DEFAULT NULL,
      `wx_openid` varchar(64) DEFAULT NULL,
      `wx_unionid` varchar(64) DEFAULT NULL,
      `remember_token` varchar(100) DEFAULT NULL,
      `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `loginip`  int(10) unsigned DEFAULT '0',
      `role` varchar(20) DEFAULT NULL,
      `yn` tinyint(1) NOT NULL DEFAULT '1',
      PRIMARY KEY (`uid`),
      UNIQUE KEY `email` (`email`),
      KEY `yn` (`yn`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;
    


    Admin 模型

    <?php
    namespace App\Models;
    
    use Illuminate\Foundation\Auth\User as Authenticatable;
    use Illuminate\Notifications\Notifiable;
    use Illuminate\Support\Facades\Hash;
    use Laravel\Passport\HasApiTokens;
    
    class Admin extends Authenticatable
    {
        use HasApiTokens, Notifiable;
    
        protected $table = 'admin';
        protected $primaryKey = 'id';
    
        protected $attributes = [
            'yn' => 1,
        ];
    
        protected $hidden = [
            'password','remember_token',
        ];
    
        public function validateForPassportPasswordGrant($password)
        {
            return Hash::check($password, $this->password);
        }
    }
    



    第十步:生成密码授权令牌

    因为前面运行了passport:install命令,则这里可以再运行此命令,直使用已经存在的 client_id:2 即可。当然也可以运行现在的命令,创建新的。

    php artisan passport:client --password
    


    执行此命令,生成名称为userToken的密码授权令牌。在db_oauth_clients数据表又生成一条记录。其他四个表,无新增数据。

    再次此命令,生成名称为adminToken的密码授权令牌。

    登录 mysql,查询记录
    select * from db_oauth_clients\G
    
    显示结果如下:
    *************************** 4. row ***************************
                        id: 4
                   user_id: NULL
                      name: userToken
                    secret: ChvTNpsjpBipUeQ6SsCzmQk7Txj9NpH9BAL6ZUlW
                  provider: user
                  redirect: http://localhost
    personal_access_client: 0
           password_client: 1
                   revoked: 0
                created_at: xxxxxx
                updated_at: xxxxxx
    *************************** 5. row ***************************
                        id: 5
                   user_id: NULL
                      name: adminToken
                    secret: w9Tg37OE68LktYVcsLPQjw14INpfeUOuygClyhUP
                  provider: admin
                  redirect: http://localhost
    personal_access_client: 0
           password_client: 1
                   revoked: 0
                created_at: xxxxxx
                updated_at: xxxxxx
    



    第十一步:创建路由中间件

    在路由上,使用 Laravel 默认的auth中间件,middleware('auth:userApi');,存在问题。对于get请求还是正常,但是postput等请求,不能通过验证,还会出现 302 跳转。所以需要自己创建中间件来加载 token 认证。

    cd /var/web/www/exampleApi
    php artisan make:middleware AuthToken
    
    <?php
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;
    
    class AuthToken
    {
        public function handle(Request $request, Closure $next, $guard = null)
        {
            //响应访问用户
    	//return response()->json(['user'=>Auth::guard($guard)->user()]);
            if($request->method() != 'OPTIONS') 
            {
                //Auth::guard($guard)->check(); 判断用户是否认证
    	    //Auth::guard($guard)->check(); 判断用户是否认证
                //Auth::guard($guard)->user() 获取用户信息
                //Auth::guard($guard)->guest() 是否是来宾
                if (empty(Auth::guard($guard)->user()))
                {
                    return response('Unauthorized.', 401);
                }
            }
            return $next($request); 
        }
    }
    


    第十二步:注册中间件

    app/Http/Kernel.php$routeMiddleware属性中添加。

    protected $routeMiddleware = [    
        'accessToken'=> \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
        'authToken'=> \App\Http\Middleware\AuthToken::class,
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
      ];
    

    accessTokenauthToken是中间件的键名,可以随便起。使用的时候,保持名称一致即可。

    在路由文件routes/api.php中,可以使用middleware('authToken:userApi')middleware('authToken:adminApi'),来管控访问路由。

    1. 路由中间件authToken,接受参数userApiadminApi,即是handle(Request $request, Closure $next,$guard = null)中的参数$guard
    2. handle方法中,Auth::guard($guard)->user(),会调用passport验证token,从而会的当前登录的用户信息。
    3. 当前端 axios 发送【预检请求】的时候,不携带 token,只有当正式请求的时候,才携带 token,所以要加条件判断$request->method()!='OPTIONS'



    第十三步:配置路由

    配置routes/api.php文件。

    <?php
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\Api;
    
    //测试
    Route::get('/', function () {
        return 'ok';
    });
    
    Route::prefix('home')->group(function () {
        Route::post('/token/client',[Api\TokenController::class,'client']);
        Route::post('/token/login',[Api\TokenController::class,'userLogin']);
        Route::middleware('accesssToken')->group(function () {
            Route::get('/topic/list',[Api\TopicController::class,'topicList']);
            Route::get('/novel/list',[Api\NovelController::class,'novelList']);     
        });
        Route::middleware('authToken:userApi')->group(function () {
            Route::get('/user/info',[Api\UserlController::class,'info']);
        });
    });
    
    Route::prefix('admin')->group(function () {
        Route::post('/token/login',[Api\TokenController::class,'adminLogin']);
        Route::middleware('authToken:adminApi')->group(function () {
            Route::get('/topic/list',[Api\TopicController::class,'topicList']);
        });   
    });
    
    
    • prefix('admin'),用于带前缀访问 URL,https://api.example.com/api/adminmiddleware('authToken:adminApi'),用于前端后台管理中心。
    • prefix('home'),用于带前缀访问 URL,https://api.example.com/api/homemiddleware('authToken:userApi'),用于前端前台用户中心。



    第十四步:请求 token 令牌

    前端注册和登录,都是由 axios 携带邮箱和密码向后端发起请求,由后端再向/oauth/token路由发出 POST 请求来获取访问令牌。而该路由已经由 Passport 提供,因此不需要手动定义它。如果请求成功,会在服务端返回的 JSON 响应中收到一个access_tokenrefresh_token

    <?php
    namespace App\Http\Controllers\Api;
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Hash;
    use Illuminate\Support\Facades\Http;
    use App\Http\Controllers\Controller;
    use App\Models\User;
    use App\Models\Admin;
    
    class TokenController extends Controller
    {
       const Msg = [
            'success' => 1,
            'error' =>  -1,
            'errorUser' =>  -11,
            'errorPassword'=>-12,
        ];
    
        
        private function checkData($request)
        {
            $request->validate([
                'email' => 'required|email',
                'password' => 'required|string',
            ]);
    
            return ['email'=>$request->input('email'),'password'=>$request->input('password'),'passwordHash'=>Hash::make($request->input('password'))];
        }
    
        public function adminLogin(Request $request)
        {
            $data=$this->checkData($request); 
            if($data)
            {
                $userObj=DB::table('admin')->select('id','password','nickname','icon','role')->where('email','=',$data['email'])->first(); 
                if($userObj)
                {
                    if (Hash::check($data['password'], $userObj->password)) 
                    {
                        $token=$this->passwordToken($data['email'], $data['password'],5);
                        $userArr=['email'=>$data['email'],'nickname'=>$userObj->nickname,'icon'=>$userObj->icon,'role'=>$userObj->role];
                        return response()->json(['msg' => '成功', 'token'=>$token, 'user'=>$userArr, 'code'=> self::Msg['success']]);
                    }
                    else
                    {
                        return response()->json(['msg' => '密码错误', 'code'=> self::Msg['errorPassword']]);
                    }
                }
                return response()->json(['msg' => '账号不存在', 'code'=> self::Msg['errorUser']]);
            }
        }
    
        public function userLogin(Request $request)
        {
            $data=$this->checkData($request); 
            if($data)
            {
                $userObj=DB::table('user')->select('uid','password','nickname','icon','role')->where('email','=',$data['email'])->first(); 
                if($userObj)
                {
                    if (Hash::check($data['password'], $userObj->password)) 
                    {
                        $token=$this->passwordToken($data['email'], $data['password'],4);
                        $userArr=['email'=>$data['email'],'nickname'=>$userObj->nickname,'icon'=>$userObj->icon,'role'=>$userObj->role];
                        return response()->json(['msg' => '成功', 'token'=>$token, 'user'=>$userArr, 'code'=> self::Msg['success']]);
                    }
                    else
                    {
                        return response()->json(['msg' => '密码错误', 'code'=> self::Msg['errorPassword']]);
                    }
                }
                return response()->json(['msg' => '账号不存在', 'code'=> self::Msg['errorUser']]);
            }
        }
    
    
       private function passwordToken($email,$password,$client_id)
        {
    	$tokenObj=DB::table('oauth_clients')->where('id','=',$client_id)->first();
    	$response = Http::asForm()->post('https://api.example.com/oauth/token', [
                        'grant_type'=>'password',
                        'client_id'=>$client_id,
                        'client_secret'=>$tokenObj->secret,
                        'username' => $email,
                        'password' => $password,
                        'scope' => '*',
    	]); 
    	return $response->json();
        }
    
        public function client()
        {
    	$tokenObj=DB::table('oauth_clients')->where('id','=',3)->first();
    	$response = Http::asForm()->post(https://api.example.com/oauth/token', [
                    'grant_type' => 'client_credentials',
                    'client_id' => 3,
                    'client_secret' => $tokenObj->secret,
                    'scope' => '*',
    	]);
        
    	 return response()->json($response->json());
        }
    
    }
    
    • 前端 axios 携带 user 登录的 email 账号和密码,向https://api.example.com/api/home/token/login发送请求,可以获得userToken令牌,以及 user 用户的用户信息(账号、昵称、头像、用户组)。
    • 前端 axios 携带 admin 登录的 email 账号和密码,向https://api.example.com/api/admin/token/login发送请求,可以获得adminToken令牌,以及 admin 用户的用户信息(账号、昵称、头像、用户组)。

    返回 token 令牌中,包含四个字段:

    • access_token:是授权令牌,可以将这个值设置到 Bearer Authentication 请求头去请求需要认证的后端 API 接口。
    • token_type:表示认证类型是Bearer
    • expires_in:表示令牌的有效期(单位是秒,默认有效期一年)。从获得到 token开始,到 xxx 秒以后失效。若有效期 15天:60 * 60 * 24 * 15 =1296000
    • refresh_token:用于在令牌过期后刷新令牌。需要设置有刷新,才会有此值。
    {"token_type":"Bearer","expires_in":1296000,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzIiwianRpIjoiNTNmNzM3ZWYzYjA4N2FkMGJiNTNmYjg5MWE1MzRlMGE3M2JkNGNkNGYxZTI5ZGYyMTRjZGIzOTU1MTQxZTU0ZDk5Mjg3NzJkYzg0YzYzOTIiLCJpYXQiOjE2NjM0ODY3NjkuMjQwNjE4LCJuYmYiOjE2NjM0ODY3NjkuMjQwNjIsImV4cCI6MTY2NDc4Mjc2OS4yMTUxNjMsInN1YiI6IiIsInNjb3BlcyI6WyIqIl19.S4tDwbtTezcWoL4LraZHdwb5B-GuM-hZ5Ns0MysLOogY98l_ZS9zxRHKQjcQbi79K6EGNnmILE7qlOC4jmyiHBhaieKNEqz7bOYWtU5jNXtFc1-axFGyy7UkGclwvTyzLsc9Uxu-c95qwAvjtcNcQWczoB6DJSQWyBPYKbVJ3o5dEq43yHEoO-iK1OeN1YeP2rs-KWCLU1KqIxEVEL5vUd0xlHFqyJgJWW83200O8EqeT9JwGbCWXeifLlbQReIJkzsj4SZe1BxwPrVOFIAUTUyaIza968Lh7vm144nMJtOAA6yct0ab8_OW8fvGVXQKR0WakUIX-X0G8qgGNWha1kxaanFC47R790elncAJOJ0_PIjY_Mxsyc7CAke_rXLQEEnKiUBgUuhsCncgK_moY-EHkF392pLRvyxh1h7uFhMJXxo9RRfaWUezXh8VwkTeuIdhW4pD_hNa1uQFEBZpqCb-u9eAshwOj39MIvh0F7ukklxpSfuZRpMp8qBAPB-gJMmV4DwNNCiHxkhpHx6eTWa_do9cSKh1prWWjlht-yNJ6mCN5SUbIeN5F7liwEPBj7dY5HTpy3bqtxB1RKtUiboUvykp_OAOnbM9cL9F6Zmv899KJVqNkhIsENObXotCjJxfFAyj3O_1IkYJOCLhEFF33kWR3FNQodATYOrUqIk"}
    




    第十五步:修改 token 有效期

    app/Providers/AuthServiceProvider.php文件,把 token(accessToken、userToken、adminToken)有效期,改成15天。

    <?php
    namespace App\Providers;
    
    //use Illuminate\Support\Facades\Gate;
    use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
    use Laravel\Passport\Passport;
    
    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The model to policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            //'App\Models\Model' => 'App\Policies\ModelPolicy',
        ];
    
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
            Passport::tokensExpireIn(now()->addDays(15));
        }
    }
    




    卸载 Passport

    由于某些特殊原因,需要卸载删除 Passport 的时候,执行一下操作。

    第一步:登录 mysql 数据库,删除响应的数据表。以下为 mysql 命令。

    drop table db_oauth_access_tokens;
    drop table db_oauth_auth_codes;
    drop table db_oauth_clients;
    drop table db_oauth_refresh_tokens;
    drop table db_oauth_personal_access_clients;
    drop table db_personal_access_tokens;
    
    若没有安装passport之外的其他依赖包,可以删除此表。否则,删除其中的记录信息。
    drop table db_migrations;
    

    第二步:删除storage目录两个密钥文件oauth-private.keyoauth-public.key

    在根目录下 执行 Linux 命令
    rm -f storage/oauth-private.key
    rm -f storage/oauth-public.key
    

    第三步:去除composer.json加载 passport 记录。

    composer.json
    ......
    "require": {
        "php": "^8.0.2",
        "guzzlehttp/guzzle": "^7.2",
        "laravel/framework": "^9.19",
        "laravel/passport": "^11.0",
        "laravel/sanctum": "^3.0",
        "laravel/tinker": "^2.7",
        "mews/captcha": "^3.2"
    }
    ......
    

    第四步:重载。

    在根目录下 执行 Linux 命令
    rm -rf database/migrations/*
    composer dump-autoload
    



    默认提供的路由控制器位于\Laravel\Passport\Http\Controllers命名空间下,并且路由前缀为/oauth,如果你想要自定义这些配置,可以在上述Passport::routes();方法中通过第二个参数传入。具体注册的 API 认证相关路由如下:


    Authorization

    passport 接受验证的header头,必须关键字Authorization,意思是 HTTP 授权的授权证书。格式:

    Authorization: Bearer <token>
    

    API 通过授权来确保客户端请求安全地访问数据。主要包括:发送者身份验证和访问权限认证。使用 Postman 接口测试工具,进行测试的时候,Authorization 选项卡中,可选类型:

    • Inheriting auth:继承认证。
    • No auth:无授权认证。
    • Bearer Token:令牌。
    • Basic auth:基本授权认证。
    • Digest auth:摘要授权。
    • OAuth 1.0
    • OAuth 2.0
    • Hawk authentication
    • AWS Signature:签名授权。
    • NLTM authentication

    Bearer Token通过使用访问密钥对请求进行身份验证。令牌是文本字符串,包含在请求标头中。在请求“Authorization ”选项卡中,从“TYPE”下拉列表中选择“Bearer Token”。在“ Token ”字段中,输入您的API密钥值,也采用变量引用方式,便于管理和安全保护。