一、前言

  1. 如果只是单纯需要使用 JWT,可以参考在下之前:Lumen5.8使用JWT【2019.06.22最新教程】,更新啦! 的教程

    二、说明

  2. 不知不觉 Lumen 已经更新到 ‘5.8.x’ 版本,因此本文也紧跟脚步,使用最新版的 Lumen 进行讲解,最重要的是 Laravel/Lumen 5.8.x 版本只支持 ‘PHP7.1.3’ 及以上

  3. 本文使用 ‘dusterio/lumen-passport’ 最新 (0.2.10) 版本
  4. 操作环境:‘Windows 10’ + ‘PHP7.2’ + ‘MariaDB10.3’。上述环境在下均已测试多次,现分享出本人至今 ‘Windows’ 下正常开发使用的 整合压缩包

    三、开始

  5. 使用 ‘Composer’ 安装最新的 Lumen 到本地。
    composer create-project laravel/lumen passport-test --prefer-dist

  6. 进入项目 ‘passport-test’ 中,安装 ‘dusterio/lumen-passport’ 到本地,0.2.9以后的版本有所改动,详细请参考文末的 附录
    composer require dusterio/lumen-passport
  7. 首先模拟 Laravel 目录结构,复制‘vender/laravel/lumen-framework’下的 ‘config 目录到 ‘passport-test’ 根路径。复制完成以后 ‘passport-test’ 的根目录结构如下:

    1. /app
    2. ......others.......
    3. /config <<<<<< 配置文件目录
    4. /vendor
    5. ......others.......
  8. 以后的配置文件,都只需要在根路径下的 ‘config’目录操作即可,修改根路径下的 ‘.env 文件:

    1. ......others.......
    2. APP_KEY=9TBF8FrZZgYBoM0AzKjkii/yb6TJVm11 #### Lumen默认没有设置APP_KEY
    3. CACHE_DRIVER=file #### 修改为文件缓存
    4. ......others (包括MySQL的配置项) .......

    本文是使用 php -S localhost:8000 public/index.php指令快速启动服务

  9. 下面开始实现 JWT 功能。由于本人习惯在 ‘app’ 路径下新建一个 ‘Api’ 目录存放接口控制器类、 ‘Models’ 目录存放模型,因此之后的项目目录结构是:

    1. ......others.......
    2. /app
    3. ..........others.......
    4. ..../Api <<<<<< 接口控制器文件目录
    5. ..../Models <<<<<< 模型文件目录
    6. /config <<<<<< 配置文件目录
    7. /vendor
    8. ......others.......
    • 修改 ‘bootstrap’ 文件夹下的 ‘app.php’ 如下所示,需要注意的是,我修改了最后一段代码,也即修改了 Lumen 默认的控制器命名空间和默认的路由文件:
      ```php

      Dir: @/bootstrap/app.php

      <?php

requireonce _DIR.’/../vendor/autoload.php’;

try { (new Dotenv\Dotenv(DIR.’/../‘))->load(); } catch (Dotenv\Exception\InvalidPathException $e) { // }

$app = new Laravel\Lumen\Application( realpath(DIR.’/../‘) );

// 取消注释 $app->withFacades(); $app->withEloquent();

$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class );

$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class );

注意!我已经修改了默认的认证中间件的路径!!

$app->routeMiddleware([ ‘auth’ => App\Api\Middlewares\Authenticate::class, ]);

// 取消注释 $app->register(App\Providers\AppServiceProvider::class); $app->register(App\Providers\AuthServiceProvider::class);

// 新增Passport的注册 $app->register(Laravel\Passport\PassportServiceProvider::class); $app->register(Dusterio\LumenPassport\PassportServiceProvider::class);

注意!我已经修改了默认的命名空间!!

$app->router->group([ ‘namespace’ => ‘App\Api\Controllers’, ], function ($router) { require DIR . ‘/../routes/api.php’; # 注意!我已经修改了默认的路由文件!! });

return $app;

  1. - 修改 **'config'** 文件夹下的 **'auth.php'** 如下所示:<br />
  2. ```php
  3. # Dir: @/config/auth.php
  4. <?php
  5. return [
  6. 'defaults' => [
  7. 'guard' => env('AUTH_GUARD', 'api'),
  8. ],
  9. 'guards' => [
  10. 'api' => ['driver' => 'passport', 'provider' => 'passport-provider'],
  11. ],
  12. 'providers' => [
  13. 'passport-provider' => [
  14. 'driver' => 'eloquent',
  15. 'model' => \App\Models\UserModel::class
  16. ]
  17. ],
  18. 'passwords' => [
  19. //
  20. ],
  21. ];
  • 修改 ‘app/Providers’ 文件夹下的 ‘AuthServiceProvider.php’ 如下所示:
    ```php

    Dir: @/app/Providers/AuthServiceProvider.php

    <?php

use Dusterio\LumenPassport\LumenPassport; use Illuminate\Support\ServiceProvider;

class AuthServiceProvider extends ServiceProvider { /**

  1. * Register any application services.
  2. *
  3. * @return void
  4. */
  5. public function register()
  6. {
  7. //
  8. }
  9. /**
  10. * Boot the authentication services for the application.
  11. *
  12. * @return void
  13. */
  14. public function boot()
  15. {
  16. LumenPassport::routes($this->app->router); # 注册Passport相关路由
  17. LumenPassport::allowMultipleTokens(); # 允许生成多个有效Token
  18. }

}

  1. - 修改 **'app/Models'** 文件夹下的 **'UserModel.php'** 如下所示:<br />
  2. ```php
  3. # Dir: @/app/Models/UserModel.php
  4. <?php
  5. namespace App\Models;
  6. use Illuminate\Auth\Authenticatable;
  7. use Laravel\Lumen\Auth\Authorizable;
  8. use Illuminate\Database\Eloquent\Model;
  9. use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  10. use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
  11. use Laravel\Passport\HasApiTokens;
  12. /**
  13. * @author AdamTyn
  14. * @description <用户>数据模型
  15. */
  16. class UserModel extends Model implements AuthenticatableContract, AuthorizableContract
  17. {
  18. use Authenticatable, Authorizable, HasApiTokens;
  19. /**
  20. * 绑定数据表
  21. * @var string
  22. */
  23. protected $table = 'users';
  24. /**
  25. * 使用模型时可以访问的字段
  26. * @var array
  27. */
  28. protected $fillable = [
  29. 'user_name', 'password',
  30. ];
  31. /**
  32. * 使用模型无法序列化为JSON时的字段
  33. * @var array
  34. */
  35. protected $hidden = [
  36. 'password',
  37. ];
  38. /**
  39. * 使用Passport用户凭证字段,数据库必须保证该字段的唯一性(默认是email)
  40. * @var string
  41. */
  42. static private $credentials = 'user_name';
  43. /**
  44. * @author AdamTyn
  45. * @description 使用用户凭证字段查询用户
  46. *
  47. * @param $username
  48. * @return $this
  49. */
  50. public function findForPassport($username)
  51. {
  52. if (!isset(self::$credentials)) {
  53. return $this->whereEmail($username)->first();
  54. }
  55. return $this->where(self::$credentials, $username)->first();
  56. }
  57. }
  • ‘app/Api/Controllers’ 文件夹下新建 ‘ClientController.php’,内容如下所示:
    ```php

    Dir: @/app/Api/Controllers/ClientController.php

    <?php

namespace App\Api\Controllers;

use Illuminate\Http\Request; use Laravel\Passport\ClientRepository; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Laravel\Lumen\Routing\Controller;

/**

  • @author AdamTyn
  • @description <用户客户端相关>控制器 / class ClientController extends Controller { /*

    • 用户客户端仓库的单例
    • @var \Laravel\Passport\ClientRepository */ private static $clientRepository = null;

      /**

    • 用户模型
    • @var \App\Models\UserModel */ private static $userModel = null;

      /**

    • 当前时间戳
    • @var int */ protected $currentDateTime;

      /**

    • 授权方式:password=密码授权的令牌,personal=私人授权的令牌
    • @var string */ protected $grantType = ‘password’;

      /**

    • @author AdamTyn *
    • AuthController constructor. */ public function __construct() { empty(self::$userModel) ? (self::$userModel = (new \App\Models\UserModel)) : true; $this->currentDateTime = time(); }

      /**

    • @author AdamTyn
    • @description 用户注册(默认密码123456) *
    • @param \Illuminate\Http\Request;
    • @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response */ public function register(Request $request) { $field = [

      1. 'user_name' => $request->get('user_name') ?? time(),
      2. 'password' => Hash::make($request->get('password') ?? '123456')

      ];

      $user = (self::$userModel)->create($field); $response[‘data’] = $user;

      return response()->json($response); }

      /**

    • @author AdamTyn
    • @description 添加使用密码认证的客户端 *
    • @param \Illuminate\Http\Request;
    • @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response */ public function registerPasswordClient(Request $request) { $response = array(‘status_code’ => ‘2000’);

      try {

      1. $user = (self::$userModel)->whereUserName($request->input('user_name'))->firstOrFail();
      2. $this->createClient($response, $request, $user);

      } catch (\Exception $exception) {

      1. $response = [
      2. 'status_code' => '5002',
      3. 'msg' => '无法响应请求,服务端异常',
      4. ];
      5. Log::error($exception->getMessage() . ' at' . $this->currentDateTime);

      }

      return response()->json($response); }

      /**

    • @author AdamTyn
    • @description 登录私人访问的客户端 *
    • @param \Illuminate\Http\Request;
    • @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response */ public function loginPersonalClient(Request $request) { $response = array(‘status_code’ => ‘2000’); $this->changeGrantType();

      try {

      1. $user = (self::$userModel)->whereUserName($request->input('user_name'))->firstOrFail();
      2. if (Hash::check($request->input('password'), $user->getAuthPassword())) {
      3. $response['data'] = $user->createToken(data_get($request, 'token_name', $user->user_name . '`s token_name'));
      4. } else {
      5. $response = [
      6. 'status_code' => '5000',
      7. 'msg' => '系统错误',
      8. ];
      9. }

      } catch (\Exception $exception) {

      1. $response = [
      2. 'status_code' => '5002',
      3. 'msg' => '无法响应请求,服务端异常',
      4. ];
      5. Log::error($exception->getMessage() . ' at' . $this->currentDateTime);

      }

      return response()->json($response); }

      /**

    • @author AdamTyn
    • @description 添加私人访问的客户端 *
    • @param \Illuminate\Http\Request;
    • @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response */ public function registerPersonalClient(Request $request) { $response = array(‘status_code’ => ‘2000’); $this->changeGrantType();

      try {

      1. $user = (self::$userModel)->whereUserName($request->input('user_name'))->firstOrFail();
      2. $this->createClient($response, $request, $user);

      } catch (\Exception $exception) {

      1. $response = [
      2. 'status_code' => '5002',
      3. 'msg' => '无法响应请求,服务端异常',
      4. ];
      5. Log::error($exception->getMessage() . ' at' . $this->currentDateTime);

      }

      return response()->json($response); }

      /**

    • @author AdamTyn
    • @description 初始化用户客户端仓库的单例 */ private function initialClientRepository() { empty(self::$clientRepository) ? (self::$clientRepository = (new ClientRepository)) : true; }

      /**

    • @author AdamTyn
    • @description 改变授权类型为私人访问 */ private function changeGrantType() { $this->grantType = ‘personal’; }

      /**

    • @author AdamTyn
    • @description 新增用户客户端记录 *
    • @param array $response
    • @param \Illuminate\Http\Request $request
    • @param \App\Models\UserModel $user */ private function createClient(&$response, $request, $user) { $this->initialClientRepository();

      if (Hash::check($request->input(‘password’), $user->getAuthPassword())) {

      1. # createPasswordGrantClient: 创建密码获取token访问的客户端记录
      2. # createPersonalAccessClient: 创建私人访问的客户端记录
      3. $func = ($this->grantType === 'password')? 'createPasswordGrantClient':'createPersonalAccessClient';
      4. $client = self::$clientRepository->$func(
      5. $user->id,
      6. $user->user_name . '`s new ' . $this->grantType . ' client',
      7. data_get($request, 'redirect', 'http://localhost:8000')
      8. );
      9. $response['data'] = [
      10. 'client_id' => $client->id,
      11. 'client_name' => $client->name,
      12. 'client_grant_type' => $this->grantType,
      13. 'client_secret' => $this->grantType ? $client->secret : null,
      14. 'client_redirect' => $client->redirect,
      15. ];

      } else {

      1. $response = [
      2. 'status_code' => '5000',
      3. 'msg' => '系统错误',
      4. ];

      } } } ```

    • 创建 ‘users’ 数据表,在数据库中简单填充一条数据。需要注意的是 Lumen 默认数据库使用 ‘utf8mb4’ 编码,如果数据库版本较低,需要修改‘app/Providers/AppServiceProvider.php’ 如下:
      ```php

      Dir: @/app/Providers/AppServiceProvider.php

      <?php

namespace App\Providers;

use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider { /**

  1. * Register any application services.
  2. *
  3. * @return void
  4. */
  5. public function register()
  6. {
  7. Schema::defaultStringLength(191);
  8. }

}

  1. - 使用以下 _Artisan_ 在数据库中新增 **'Passport'** 的相关数据表,并且填充2条测试数据:<br />
  2. ```php
  3. php artisan migrate
  4. php artisan passport:install <<<在oauth_clients表中填充了2条测试数据,并且生成了加密用的Key
  1. 运行测试。在‘routers/api.php’ 中添加相应的路由规则 ```php

    Dir: @/routers/api.php

    <?php

模拟用户注册路由

$router->post(‘register’, ‘ClientController@register’);

模拟登录私人访问的客户端

$router->post(‘loginPersonalClient’, ‘ClientController@loginPersonalClient’);

模拟添加使用密码认证的客户端

$router->post(‘registerPasswordClient’, ‘ClientController@registerPasswordClient’);

模拟添加私人访问的客户端

$router->post(‘registerPersonalClient’, ‘ClientController@registerPersonalClient’);

Passport相关的路由已经被组件定义,无需自己定义

  1. 7. 最后,可以得出以下测试结果
  2. ```php
  3. HTTP/1.1 200 OK. POST http://127.0.0.1:8000/register
  4. Request: user_name='testman', password='123456'
  5. {
  6. "status_code": "2000",
  7. "data": {
  8. "user_name": "testman",
  9. "updated_at": "2019-02-13 09:31:14",
  10. "created_at": "2019-02-13 09:31:14",
  11. "id": 5
  12. }
  13. }
  1. HTTP/1.1 200 OK. POST http://127.0.0.1:8000/registerPasswordClient
  2. Request: user_name='testman', password='123456'
  3. {
  4. "status_code": "2000",
  5. "data": {
  6. "client_id": 21,
  7. "client_name": "testman`s new password client",
  8. "client_grant_type": "password",
  9. "client_secret": "hojrGJUQMzXAm9e9r7q1K01I1Rx8rWP3LgV5xW4a",
  10. "client_redirect": "http://localhost:8000"
  11. }
  12. }
  1. HTTP/1.1 200 OK. POST http://127.0.0.1:8000/registerPersonalClient
  2. Request: user_name='testman', password='123456'
  3. {
  4. "status_code": "2000",
  5. "data": {
  6. "client_id": 22,
  7. "client_name": "testman`s new personal client",
  8. "client_grant_type": "personal",
  9. "client_secret": null,
  10. "client_redirect": "http://localhost:8000"
  11. }
  12. }
  1. HTTP/1.1 200 OK. POST http://127.0.0.1:8000/loginPersonalClient
  2. Request: user_name='testman',token_name='testman_token', password='123456'
  3. {
  4. "status_code": "2000",
  5. "data": {
  6. "accessToken": "eyJ0eXiIsImp0aSI6IjRkYmQ4ZGZDM5YmVmOTVlYzk4ZW6c",
  7. "token": {
  8. "id": "4dbd8dd35a8c109a40629d9a204915e7a22db0bd6dc",
  9. "user_id": 2,
  10. "client_id": 20,
  11. "name": "testman_token",
  12. "scopes": [],
  13. "revoked": false,
  14. "created_at": "2019-02-13 09:33:50",
  15. "updated_at": "2019-02-13 09:33:50",
  16. "expires_at": "2020-02-13 09:33:50"
  17. }
  18. }
  19. }
  1. HTTP/1.1 200 OK. GET http://127.0.0.1:8000/oauth/tokens
  2. Authorization: Bearer eyJ0eXiIsImp0aSI6IjRkYmQ4ZGZDM5YmVmOTVlYzk4ZW6c
  3. [
  4. {
  5. "id": "5fc4cf22e2b5386660659775fc5919",
  6. "user_id": 2,
  7. "client_id": 20,
  8. "name": "token_name",
  9. "scopes": [],
  10. "revoked": false,
  11. "created_at": "2019-02-13 08:52:31",
  12. "updated_at": "2019-02-13 08:52:31",
  13. "expires_at": "2020-02-13 08:52:31",
  14. "client": {
  15. "id": 20,
  16. "user_id": null,
  17. "name": "test",
  18. "redirect": "http://localhost",
  19. "personal_access_client": true,
  20. "password_client": false,
  21. "revoked": false,
  22. "created_at": "2019-02-13 08:48:48",
  23. "updated_at": "2019-02-13 08:48:48"
  24. }
  25. }
  26. ]
  1. HTTP/1.1 200 OK. GET http://127.0.0.1:8000/oauth/clients
  2. Authorization: Bearer eyJ0eXiIsImp0aSI6IjRkYmQ4ZGZDM5YmVmOTVlYzk4ZW6c
  3. [
  4. {
  5. "id": 3,
  6. "user_id": 1,
  7. "name": " Password Grant Client",
  8. "secret": "FfA04DRjDA9iMi4RJ9ju9mBtmKDyu4HbHYXRCoKD",
  9. "redirect": "http://localhost",
  10. "personal_access_client": false,
  11. "password_client": true,
  12. "revoked": false,
  13. "created_at": "2019-02-13 14:43:58",
  14. "updated_at": "2019-02-13 14:43:58"
  15. }
  16. ]
  1. 放出参考的示例 代码仓库。至此,Lumen5.8使用Passport 讲解结束,给出所有 Passport 中预设的路由规则:

Lumen5.8使用Passport【2019.06.22更新】 - 图1

四、附录

  1. 针对 dusterio/lumen-passport >= 0.2.10 的扩展包,改动位置:\Dusterio\LumenPassport\LumenPassport::routes()

    1. public static function routes($callback = null, array $options = [])
    2. {
    3. # 这一行代码导致Lumen5.8之后注册路由时,要特别指定 $this->app->router
    4. if ($callback instanceof Application && preg_match('/5\.[5-7]\..*/', $callback->version())) $callback = $callback->router;
    5. # ......
    6. # *********************** others ***********************
    7. }

    所以本文示例代码文件 ‘AuthServiceProvider.php’ 中的 boot 方法做出了相应的调整:

    1. public function boot()
    2. {
    3. LumenPassport::routes($this->app->router); # 注册Passport相关路由
    4. LumenPassport::allowMultipleTokens(); # 允许生成多个有效Token
    5. }
  2. 改动位置:\Laravel\Passport\ClientRepository->create()

    1. public function create($userId, $name, $redirect, $personalAccess = false, $password = false)
    2. {
    3. # ......
    4. # ****************** 除了保留create方法以外,还特意区分了 [密码获取token访问的客户端] 和 [私人访问的客户端] ******************
    5. }
    6. public function createPersonalAccessClient($userId, $name, $redirect)
    7. {
    8. return tap($this->create($userId, $name, $redirect, true), function ($client) {
    9. $accessClient = Passport::personalAccessClient();
    10. $accessClient->client_id = $client->id;
    11. $accessClient->save();
    12. });
    13. }
    14. public function createPasswordGrantClient($userId, $name, $redirect)
    15. {
    16. return $this->create($userId, $name, $redirect, false, true);
    17. }

    所以本文示例代码文件 ‘ClientController.php’ 中的 createClient 方法做出了相应的调整:

    1. private function createClient(&$response, $request, $user)
    2. {
    3. # ......
    4. # *********************** others ***********************
    5. # createPasswordGrantClient: 创建密码获取token访问的客户端记录
    6. # createPersonalAccessClient: 创建私人访问的客户端记录
    7. $func = ($this->grantType === 'password')? 'createPasswordGrantClient':'createPersonalAccessClient';
    8. $client = self::$clientRepository->$func(
    9. $user->id,
    10. $user->user_name . '`s new ' . $this->grantType . ' client',
    11. data_get($request, 'redirect', 'http://localhost:8000')
    12. );
    13. # ......
    14. # *********************** others ***********************
    15. }

    五、结语

  3. 本教程面向新手,更多教程会在日后给出。

  4. 随着系统升级,软件更新,以后的配置可能有所变化,在下会第一时间测试并且更新教程。
  5. 欢迎联系在下,讨论建议都可以,之后会发布其它的教程。
  6. 如果读者使用的是旧版本 Lumen,可以参考本人之前出了 Lumen5.7使用Passport【2019.02.13最新教程】,更新啦 教程。
  7. 后面紧锣密鼓地将会推出 Laravel业务篇 系列的教程,敬请期待。