PHP & Laravel & 掌握 api 生成 token 的几种方式以及一些注意事项(坑)
介绍
本章略长,采用了 3 种创建 token 方式,读者可以选择任意一节阅读,但本人建议全部看完,掌握多种生成 token 方式何乐而不为呢。
准备工作
- 创建 Laravel 项目并命名为 example-app
composer create-project laravel/laravel example-appcd example-appphp artisan serve
没有特殊情况的话可以看到项目已正常运行输出
Starting Laravel development server: http://127.0.0.1:8000
- 本章所使用的 php 版本是 7.3
- 本章所使用的 Laravel 版本是 8X ,Laravel 7X 没有试过。
1. 使用 Sanctrum
Laravel 默认采用 web session 认证机制,没有提供 api 认证,但最新版 Laravel 中内置了 santum,它是专门用来 api 认证生成 token 的扩展包,不过需要自己配置才能使用。
1.1 配置数据库
sanctum
对 token 的管理是在数据库中,我们还需要到 .env
环境变量文件里进行配置
DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306# 填上你的数据库名、数据库用户、数据库密码DB_DATABASE=productDB_USERNAME=rootDB_PASSWORD=123456
1.2 安装 sanctum
1 下载 sanctum
提示:最新 Laravel 已经提前下载好 sanctum 我们可以在 compose.json 中查看,如果没有找到则可以使用下面命令下载
# 下载 sanctumcomposer require laravel/sanctum# 发布并更新配置# 修改内容包括 migrage 、app/Models/User.php、以及 routes/api.php php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"# 生成 sanctum 定义好的表php artisan migrate
2 配置 config/auth.php
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], // 新增 api 士兵 'api' => [ 'driver' => 'sanctum', 'provider' => 'users', ], ],
1.3 新增创建 token 接口
routes/api.php
代码如下
use Illuminate\Http\Request;use Illuminate\Support\Facades\Route;use App\Models\User;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{ protected function redirectTo($request) { if (! $request->expectsJson()) { return route('login'); } }}
由于我们是针对 api 不是 web ,不需要重定向,这里可以重写一下逻辑。
class Authenticate extends Middleware{ 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
1.1 下载 jwt-auth
composer require tymon/jwt-auth
1.2 在 config/app.php
新增服务
'providers' => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class,]
1.3 发布配置
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
1.4 生成秘钥
# 该秘钥会放到 .env 变量环境里面,JWT_SECRET = xxxxphp artisan jwt:secret
2.2 配置 jwt-auth
2.1 在 app/Models/User.php
User 模型中实现 JWTSubject 接口
//...省略use Tymon\JWTAuth\Contracts\JWTSubject; // 引入接口class User extends Authenticatable implements JWTSubject{// ...省略// 将官方提供实现接口的两个方法搬过来放到这里 public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; }}
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.1 新建 AuthController.php
文件(名字随便定义)
php artisan make:controller AuthController
2.2 app/Http/Controolers/AuthController.php
代码如下
namespace App\Http\Controllers;use Illuminate\Http\Request;use App\Models\User;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.3 配置 routes/api.php
路由
use Illuminate\Support\Facades\Route;use App\Http\Controllers\AuthController;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 配置 routes/api.php
路由
use Illuminate\Support\Facades\Route;use App\Http\Controllers\AuthController;Route::post('/tokens/create', [AuthController::class, 'create']);// 新增 getProfileRoute::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 Illuminate\Auth\AuthenticationException; // 引入class Handler extends ExceptionHandler {// ... 省略// 新增这个方法protected function unauthenticated($request, AuthenticationException $exception) { return response([ 'msg' => '未授权,请先登录', 'code' => -10000 ]); }}
再来看看效果:
2.4 jwt.php 配置文件
1 设置 token 过期时间
// 读取 JWT_TTL,没有的话默认过期时间为 60 分钟。'ttl' => env('JWT_TTL', 60)
2 设置刷新 token 时间有效期限
// 默认 token 在 2 周内都可以进行刷新重复使用。'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
其它配置具体就不详细多说了, 可参考官方文档。
3. 使用 firebase/php-jwt
准备工作
- 还是老样子,我们新建一个 Laravel 项目并配置好数据库,怎么操作可翻到最顶部观看。
- firebase/php-jwt 库要求 php7 以上
3.1 安装 php-jwt
composer require firebase/php-jwt
3.3 新增创建 token 接口
3.1 配置 routes/api.php
路由
use Illuminate\Support\Facades\Route;use Firebase\JWT\JWT;use App\Models\User;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/sanctum
和 tymon/jwt-auth
都已经内置好这些功能了,这里我们需要自己手动搞一个。
在动手前我们先回顾前面两种获取用户信息接口时用到哪些东西,貌似也就多了 middleware('auth:api');
这句话,其它没什么变化吧?为避免有些读者刚入门,我还是解释一下这句话的含义吧:
- auth 是一个中间件,可以在
app/Http/Kernel.php
中的$routeMiddleware
属性找到,它映射了\App\Http\Middleware\Authenticate::class
中间件。 - 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)
函数来定义士兵即可, 这是最快捷的一种方式,其它的就不细说了,后续我会专门开一篇文章来讲解士兵相关内容,现在不懂这些概念也没关系,用的多了就懂了,我们先让功能能用起来再说。
1 在 app/Providers/AuthServiceProvider.php
文件中改动如下:
namespace App\Providers;use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;use Illuminate\Support\Facades\Auth;use App\Models\User;use Firebase\JWT\JWT;use Firebase\JWT\Key;use Illuminate\Http\Request;class AuthServiceProvider extends ServiceProvider{ protected $policies = [ // 'App\Models\Model' => 'App\Policies\ModelPolicy', ]; 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; }); }}
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 在 app/Http/Middleware/Authenticate.php
(也就是 auth 中间件)改动如下:
<?phpnamespace App\Http\Middleware;use Illuminate\Auth\Middleware\Authenticate as Middleware;use Closure;use Illuminate\Http\Request;use Firebase\JWT\JWT;use Firebase\JWT\Key;use Illuminate\Support\Facades\Auth;class Authenticate extends Middleware{ public function handle(Request $request, Closure $next) { try { $token = $request->header('token'); $key = 'hello'; if (empty($token)) { return response([ 'msg' => '缺少 token', 'code' => -10000 ]); } // 尝试解析,如果解析成功则可以进入 next 反之进入 catch 捕获异常 $payload = JWT::decode($token, new Key($key, 'HS256')); } catch(\Firebase\JWT\ExpiredException $e) { return response([ 'msg' => 'token 已过期', 'code' => -10000 ]); } catch(\Exception $e) { return response([ 'msg' => 'token 格式有误: ' . $e->getMessage(), 'code' => -20000 ]); } // 验证通过 return $next($request); }}
handle 方法是每个中间件都自带的,auth 中间件自然也不例外。
3.4 新增获取用户信息接口
1 配置 routes/api.php
路由
// 新增这段Route::post('getProfile', function() { // auth() 默认是 web,现在已经改成 api了,无需再指定 auth()->guard('api')->user() $user = auth()->user(); return response([ 'data' => $user ]);})->middleware('auth:api');
接下来使用不同的 token 来请求 http://localhost:8000/api/getProfile
2 传递空的 token
3.4.2 传递格式错误的 token
3.4.4 传递已过期的 token:这里分为几步骤
a) 将原来的创建 token 接口代码稍作改动下,将创建用户改为查找用户,把过期时间改为 5 秒,代码如下
Route::post('tokens/create', function() { // 前面已经创建过了,我们只需找到这位用户即可。 $user = User::where([ 'name' => 'cookcyq3', ])->first(); $key = "hello"; $payload = array( 'user_id' => $user->id, 'user_name' => $user->name, // token 过期时间为 5秒 'exp' => time() + 5 ); $jwt = JWT::encode($payload, $key, 'HS256'); return response([ 'token' => $jwt ]);});
b) 请求获取 token 接口(token 5秒后就过期)
c) 5秒过后,将 token 传递请求用户信息接口:
d) 传递正确且有效的 token:
PS:将上面的 token 过期时间设置长一点重新获取一遍 token 即可。
总结
- sanctum 和 jwt-auth 都是集成好的扩展包,上手快,开箱即用,安全性处理好。
- firebase/php-jwt 偏向自定义风格,如 token 验证、token 的解/编码,自定义士兵等,如果你对中间件、士兵这些抽概念还不清楚的话,选择前面任意一种使用就可以了。
- 具体用哪种因人而异,本人偏向 firebase/php-jwt 和 tymom/jwt-auth。
好了本文就到这里,有问题欢迎指出,喜欢的话可以点赞收藏。
文献:
https://laravel.com/docs/8.x/sanctum
https://jwt-auth.readthedocs.io/en/develop/quick-start/
https://github.com/firebase/php-jwt
来源地址:https://blog.csdn.net/cookcyq__/article/details/124214034
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341