介绍
本章略长,采用了 3 种创建 token 方式,读者可以选择任意一节阅读,但本人建议全部看完,掌握多种生成 token 方式何乐而不为呢。
准备工作
- 创建 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
- 本章所使用的 php 版本是 7.3
- 本章所使用的 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="Laravel\Sanctum\SanctumServiceProvider"
# 生成 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 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
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $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 \Illuminate\Http\Request $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' => [
...
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
2.1.3 发布配置
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
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 Tymon\JWTAuth\Contracts\JWTSubject; // 引入接口
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 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.2.3 配置 routes/api.php
路由
<?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.2 配置 routes/api.php
路由
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
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 Illuminate\Auth\AuthenticationException; // 引入
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
准备工作
- 还是老样子,我们新建一个 Laravel 项目并配置好数据库,怎么操作可翻到最顶部观看。
- firebase/php-jwt 库要求 php7 以上
3.1 安装 php-jwt
composer require firebase/php-jwt
3.3 新增创建 token 接口
3.3.1 配置 routes/api.php
路由
<?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)
函数来定义士兵即可, 这是最快捷的一种方式,其它的就不细说了,后续我会专门开一篇文章来讲解士兵相关内容,现在不懂这些概念也没关系,用的多了就懂了,我们先让功能能用起来再说。
3.4.1 在 app/Providers/AuthServiceProvider.php
文件中改动如下:
<?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
{
/**
* The 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();
// 新增
// 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 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
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
/**
* jwt 的过期、签名错误的底层处理是通过抛异常机制实现的,
* 所以我们要通过捕获的形式来响应。
*/
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 新增获取用户信息接口
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
3.4.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