网络知识 娱乐 PHP & Laravel & 掌握 api 生成 token 的几种方式以及一些注意事项(坑)

PHP & Laravel & 掌握 api 生成 token 的几种方式以及一些注意事项(坑)

介绍

本章略长,采用了 3 种创建 token 方式,读者可以选择任意一节阅读,但本人建议全部看完,掌握多种生成 token 方式何乐而不为呢。

准备工作

  1. 创建 Laravel 项目并命名为 example-app
composer create-project laravel/laravel example-app
cd example-app
php artisan serve

没有特殊情况的话可以看到项目已正常运行输出

Starting Laravel development server: http://127.0.0.1:8000
  1. 本章所使用的 php 版本是 7.3
  2. 本章所使用的 Laravel 版本是 8X ,Laravel 7X 没有试过。

1. 使用 Sanctrum

Laravel 默认采用 web session 认证机制,没有提供 api 认证,但最新版 Laravel 中内置了 santum,它是专门用来 api 认证生成 token 的扩展包,不过需要自己配置才能使用。

1.1 配置数据库

sanctum 对 token 的管理是在数据库中,我们还需要到 .env 环境变量文件里进行配置

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
# 填上你的数据库名、数据库用户、数据库密码
DB_DATABASE=product
DB_USERNAME=root
DB_PASSWORD=123456

1.2 安装 sanctum

1.2.1 下载 sanctum

提示:最新 Laravel 已经提前下载好 sanctum 我们可以在 compose.json 中查看,如果没有找到则可以使用下面命令下载

# 下载 sanctum
composer require laravel/sanctum
# 发布并更新配置
# 修改内容包括 migrage 、app/Models/User.php、以及 routes/api.php 
php artisan vendor:publish --provider="LaravelSanctumSanctumServiceProvider"
# 生成 sanctum 定义好的表
php artisan migrate

1.2.2 配置 config/auth.php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // 新增 api 士兵
        'api' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
    ],

1.3 新增创建 token 接口

routes/api.php 代码如下

<?php
use IlluminateHttpRequest;
use IlluminateSupportFacadesRoute;
use AppModelsUser;
Route::post('/tokens/create', function (Request $request) {
    # 创建用户,这里写死作为案例。
    $user = User::create([
        'name' => 'cookcyq',
        'email' => '10086@qq.com',
        'password' => Hash::make('123457')
    ]);
    # 给 cookcyq 用户生成 token, $key 是秘钥,平时秘钥一定设置复杂点,这里仅作为案例。
    $key = 'hello';
    $token = $user->createToken($key);
    # 返回
    return ['token' => $token->plainTextToken];
});

接下来使用 postman 访问 http://localhost:8000/api/tokens/create
注意要带上 api 前缀
效果如图:
在这里插入图片描述
可以看到成功创建用户并返回 token,现在来看看users 表是存在此用户
在这里插入图片描述
再来看看 personal_access_tokens 表,这个表就是 sanctum 定义的,我们来看看是否存放用户对应的 token 相关字段信息
在这里插入图片描述
如图所示一切正常,你可能注意到 personal_access_tokens 表里的 token 内容与 postman 返回的token 不一致,这个无需担心,这是 sanctum 自己要处理的逻辑,我们只需拿接口返回的 token 去使用即可。

1.4 新增获取用户信息接口

拿到 token 后,我们开始新增用户信息接口来验证 token 是否对应上该用户。
routes/api.php 代码如下

// ...
// 以上省略

// 新增
Route::post('/getProfile', function (Request $request) {
	// 获取用户信息:sanctum 帮我们从数据库中寻找,它能寻找是因为我们已经在 auth.php 中配置好 provider:users 对应的 Elquent User 模型
    $user = $request->user();
    // 也可以用以下方式获取
    // $user = auth()->guard('api')->user();
    
    return response([
        'data' => $user
    ]);
})->middleware('auth:api');

现在拿刚才接口返回的 token 去访问http://localhost:8000/api/getProfile
sanctum 是采用 Bearer Token 形式,需要带上 Bearer 前缀,header 请求格式如下:
Authorization: Bearer 1|Vjq5FOkhnwX6laVxNLE2YAEZTrMopmQeHtC4KyA2

访问效果图:
在这里插入图片描述
可以看到根据 token 可以返回对应的用户信息,现在我们用无效的 token 试试
注意:在使用前,确保 postman 里的 header 设置为 Accept:application/json
否则会报如下错误:Route[login] not defined.
在这里插入图片描述
这个报错是因为 Laravel 默认情况下会对 Access 做出相应的认证判断,由于 postman header 默认设置为 Access: * ,而 Laravel 默认的授权认证是采用 web session 机制,所以未授权的用户都会重定向到 login 页面,触发逻辑代码可在 app/http/Middleware/Authenticate.php 中看到

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  IlluminateHttpRequest  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('login');
        }
    }
}

由于我们是针对 api 不是 web ,不需要重定向,这里可以重写一下逻辑。

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  IlluminateHttpRequest  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            // 换成这句
            return response([
				'msg' => '请登录',
				'code' => -10000
			]);
        }
    }
}

现在继续拿错误的 token 来访问,正常来讲会按照上面的格式来返回吧?然而并没有,看图
在这里插入图片描述

报了另外一个错误:ErrorException: Header may not contain more than a single header, new line detected in file …
这个错误的根源就是上面提到:postman 的 header 没有设置 Accept:application/json 而导致的。
好了,现在我们设置下看看效果。
在这里插入图片描述
错误倒是没有了,但返回的格式跟上面写的也不一样啊,难道 redirectTo 函数没有触发?触发是有的,只是没有进 if (! $request->expectsJson()) {} 这句判断,正是 postman 的 header 没有设置相应的 Access 导致阴差阳错触发了 Laravel 默认对 header Access 处理的机制,也就是说这句判断压根就不是为 api 服务的,是给 web session 提供的,所以 redirectTo 函数我们可以注释掉。

现在我们希望能按照上面的格式返回应该怎么做?实现方式有几种,这里简单用 Laravel 提供的 unauthenticated 方法,还是在app/http/Middleware/Authenticate.php 里面修改

protected function redirectTo($request){ // ....}
// 新增这个方法
protected function unauthenticated($request, array $guards)
    {
        abort(response()->json([
            'code' => -10000,
            'msg' => '请登录'
         ]) );
    }

现在来看看效果:
在这里插入图片描述
经过了一般折腾终于正常了,此方法在最新 7X 8X 9X 文档中没有呈现,我是在 5.7 X 发现的 ,说真的, Laravel 文档对于刚入门的初学者来说我觉得不太友好, 上手起来总会遇到额外的情况需要自己去摸索,由于 Laravel 框架内置功能太多,这不后来新增了 Laravel/lumen 框架,此框架去掉了许多 Laravel 内置功能,上手较快,感兴趣的同学可以自行了解。

2. 使用 tymon/jwt-auth

准备工作

为了让案例易于理解,本文将继续新建 Laravel 项目,然后配置数据库,这些操作就不演示了,具体可翻到最顶部查看如何操作。

2.1 安装 jwt-auth

2.1.1 下载 jwt-auth

composer require tymon/jwt-auth

2.1.2 在 config/app.php 新增服务

'providers' => [
    ...
    TymonJWTAuthProvidersLaravelServiceProvider::class,
]

2.1.3 发布配置

php artisan vendor:publish --provider="TymonJWTAuthProvidersLaravelServiceProvider"

2.1.4 生成秘钥

# 该秘钥会放到 .env 变量环境里面,JWT_SECRET = xxxx
php artisan jwt:secret

2.2 配置 jwt-auth

2.2.1 在 app/Models/User.php User 模型中实现 JWTSubject 接口

//...省略
use TymonJWTAuthContractsJWTSubject; // 引入接口
class User extends Authenticatable implements JWTSubject{
	
	// ...省略
	
	
	// 将官方提供实现接口的两个方法搬过来放到这里
	/**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

2.2.2 配置 config/auth.php

....
'defaults' => [
	 // 将 api 作为默认士兵 ,这样每次使用 auth() 或 Auth:: 就是 api 而不是 web 了。
     'guard' => 'api',
     'passwords' => 'users',
 ],
'guards' => [
     'web' => [
          'driver' => 'session',
          'provider' => 'users',
      ],
      // 新增 api 士兵
      'api' => [
          'driver' => 'jwt',
          'provider' => 'users',
      ],
  ],

2.2 新增创建 token 接口

2.2.1 新建 AuthController.php 文件(名字随便定义)

php artisan make:controller AuthController

2.2.2 app/Http/Controolers/AuthController.php 代码如下

<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppModelsUser;
use JWTAuth; // 使用 JWT 库
class AuthController extends Controller
{
	// 创建用户并生成对应的 token
    public function create() {
        $data = [
            'name' => 'Cookcyq2',
            'email' => '100862@qq.com',
            'password' => bcrypt('1234567')
        ];
        $user = User::create($data);
        $token = JWTAuth::fromUser($user);
        // 返回 token 
        return response([ 
            'token' => $token,
            'token_type' => 'bearer',
            // 过期时间
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

2.2.3 配置 routes/api.php 路由

<?php
use IlluminateSupportFacadesRoute;
use AppHttpControllersAuthController;
Route::post('/tokens/create', [AuthController::class, 'create']);

现在我们来访问:http://localhost:8000/api/tokens/create
注意要带上 api 前缀
效果如图:
在这里插入图片描述
一切正常,此时数据库中也有对应的用户
在这里插入图片描述

2.3 新增获取用户信息接口

现在我们来验证 token 是否对应上用户信息
2.2.2 app/Http/Controolers/AuthCroller.php 代码如下

class AuthController extends Controller {
	public function create() { ... }
	// 新增 getProfile 方法
	public function getProfile() {
        return response([
            'data' => auth()->user()
        ]);
    }
}

2.2.2 配置 routes/api.php 路由

<?php
use IlluminateSupportFacadesRoute;
use AppHttpControllersAuthController;
Route::post('/tokens/create', [AuthController::class, 'create']);

// 新增 getProfile
Route::post('/getProfile', [AuthController::class, 'getProfile'])->middleware('auth:api');

接下来访问 http://localhost:8000/api/getProfile
注意:postman 的 header 的 Access 要设置为:Accept:application/json
效果如图:
在这里插入图片描述
可以看到 token 是正确的并返回相应的用户信息,现在我们用无效的 token 试试。
效果如图:
在这里插入图片描述
可以看到中间件拦截到并响应未授权信息,如果你想自定义响应格式可以到 app/Exceptions/Handle.php 配置如下:

// ... 省略
use IlluminateAuthAuthenticationException; // 引入
class Handler extends ExceptionHandler {
	// ... 省略
	// 新增这个方法
	protected function unauthenticated($request, AuthenticationException $exception)
	    {
	        return response([
	            'msg' => '未授权,请先登录',
	            'code' => -10000
	        ]);
	    }
}

再来看看效果:
在这里插入图片描述

2.4 jwt.php 配置文件

2.4.1 设置 token 过期时间

// 读取 JWT_TTL,没有的话默认过期时间为 60 分钟。
'ttl' => env('JWT_TTL', 60) 

2.4.2 设置刷新 token 时间有效期限

// 默认 token 在 2 周内都可以进行刷新重复使用。
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),

其它配置具体就不详细多说了, 可参考官方文档。

3. 使用 firebase/php-jwt

准备工作

  1. 还是老样子,我们新建一个 Laravel 项目并配置好数据库,怎么操作可翻到最顶部观看。
  2. firebase/php-jwt 库要求 php7 以上

3.1 安装 php-jwt

composer require firebase/php-jwt

3.3 新增创建 token 接口

3.3.1 配置 routes/api.php 路由

<?php
use IlluminateSupportFacadesRoute;
use FirebaseJWTJWT;
use AppModelsUser;
Route::post('tokens/create', function() {
    // 创建用户
    $user = User::create([
        'name' => 'cookcyq3',
        'email' => '1008666@qq.com',
        'password' => bcrypt('1234567')
    ]);
    // 秘钥,实际使用时记得设置复杂些。
    $key = "hello";
    // 元数据
    $payload = array(
        // 用户 id 用来解 token 时所需要的关键信息。
        'user_id' => $user->id,
        'user_name' => $user->name,
        // token 过期时间,这里设置一个小时。
        'exp' => time() + 3600
        // 你还可以添加任意元数据
        // ....
    );
    // 生成 token
    $jwt = JWT::encode($payload, $key, 'HS256');
    return response([
        'token' => $jwt 
    ]);
});

接下来使用 postman 访问 http://localhost:8000/api/tokens/create
效果如图:
在这里插入图片描述
再来看看用户是否存在数据库中
在这里插入图片描述

3.4 验证 token

在定义获取用户信息接口前,我们还面临验证 token 的问题,只有 token 有效我们才能将用户信息传递给接口,无效的 token 则响应未授权信息,前面介绍的 laravel/sanctumtymon/jwt-auth 都已经内置好这些功能了,这里我们需要自己手动搞一个。

在动手前我们先回顾前面两种获取用户信息接口时用到哪些东西,貌似也就多了 middleware('auth:api'); 这句话,其它没什么变化吧?为避免有些读者刚入门,我还是解释一下这句话的含义吧:

  1. auth 是一个中间件,可以在 app/Http/Kernel.php 中的 $routeMiddleware 属性找到,它映射了 AppHttpMiddlewareAuthenticate::class 中间件。
  2. api 是使用士兵的名字,也就是我们在 config/auth.php 中定义的。

这个 auth 中间件可以理解,但是这个 api 士兵的真正作用到底是干嘛的呢?为什么要指定 api? 直接用 auth 不行么?这是因为 auth 中间件默认情况下会分配一位士兵,这个士兵就是 web ,所以如果你把 api 去掉就等同于 middleware('auth:web') ,很明显我们并不需要 web 士兵,否则当你验证 token 时又会报什么Route [login] not defined. 的错误了。

只有 auth:士兵名,如果是自定义中间件,则格式为 中间件:参数,这些参数对应中间件 handle 方法第的三个参数,具体使用细节就不细说了,可以参考文档。

现在我们知道 auth 是 Laravel 内置的中间件,拿来就用,我们只差一个类似 api 的士兵,我们仔细观察 api 下面还有个 driver 和 provider,这个 driver 可以理解为引入真正的士兵,而 provider 则是 user 用户数据模型,user 也有了,我们只需创建 driver 士兵不就可以了?Laravel 提供了几种自定义士兵的方式,我们使用其中的 Auth::viaRequest(guard_name, callback) 函数来定义士兵即可, 这是最快捷的一种方式,其它的就不细说了,后续我会专门开一篇文章来讲解士兵相关内容,现在不懂这些概念也没关系,用的多了就懂了,我们先让功能能用起来再说。

3.4.1 在 app/Providers/AuthServiceProvider.php 文件中改动如下:

<?php

namespace AppProviders;
use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
use IlluminateSupportFacadesAuth;
use AppModelsUser;
use FirebaseJWTJWT;
use FirebaseJWTKey;
use IlluminateHttpRequest;
class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'AppModelsModel' => 'AppPoliciesModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        
        // 新增
		// jwt 就是创建士兵的名字,后续通过 driver:jwt 引入。
        Auth::viaRequest('jwt', function (Request $request) {
            try {
	            // 根据 token 找到用户并 return $user;
	        	// 这样就可以通过 Auth::user() 来获取对应的用户数据。
	        	// 如果 return null,则 Auth::user() 返回的就是 null
	        	// 暂且理解为 Auth::xx 系列方法就是由 jwt 士兵提供的。
                $token = $request->header('token');
                $key = 'hello';
                if (!$token) {
                    return null;
                }
                $payload = JWT::decode($token, new Key($key, 'HS256'));
                $user = User::where('id', $payload->user_id)->first();
                
                return $user;
            } catch(Exception $e) {
                return null;
            }
            return null;
        });
    }
}

3.4.2 在 config/auth.php 改动如下:

 'defaults' => [
        'guard' => 'api', // 默认是 web,这里改成 api
        'passwords' => 'users',
    ],
'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // 此士兵用于 api 的
        'api' => [
            'driver' => 'jwt', // 引入这位士兵
            'provider' => 'users'  
        ],
        // 此士兵是用于 admin 的
        'admin' => [
            'driver' => 'jwt', // 引入这位士兵
            'provider' => 'users'  
        ]
    ],

现在我们可以理解为 api / admin 就是士兵具体应用场景分类所抽象出来的别名

士兵搞好了,我们还差 token 验证,如果 token 失效则返回未授权信息。

3.4.3 在 app/Http/Middleware/Authenticate.php (也就是 auth 中间件)改动如下:

<?php
<?php

namespace AppHttpMiddleware;

use IlluminateAuthMiddlewareAuthenticate as Middleware;
use Closure;
use IlluminateHttpRequest;
use FirebaseJWTJWT