Laravel快速接入JWT用户认证(多用户认证)tymon/jwt-auth
原创JWT应用
JWT 是 JSON Web Token 的缩写,它是一个规范,让用户和服务器之间传递安全可靠的信息。
创建新项目
创建一个 Laravel的新项目,我们依然推荐大家使用 LTS 的版本:
composer create-project --prefer-dist laravel/laravel laravel-test
- 配置站点并修改 host文件
- 修改一下 env
创建一下基础的数据表。
php artisan migrate
这样项目就可以正常访问了。
安装
安装一下扩展包 tymon/jwt-auth
composer require tymon/jwt-auth
不发布配置也是可以使用的,可以直接通过 env 变量修改,为了方便之后的讲解,我们发布出出来。
php artisan vendor:publish --provider="TymonJWTAuthProvidersLaravelServiceProvider"
执行一下 jwt:secret ,这个命令会在 env 中增加一个 JWT_SECRET ,同我们的 APP_KEY 这个 secret 是十分重要的,用于给 Token 签名,更换这个 secret 会导致之前生成的所有 Token 无效,所以不要随意的更换这个 secret
php artisan jwt:secret
快速接入
创建 Token
修改一下 User 模型,需要实现扩展包提供的接口 Tymon\JWTAuth\Contracts\JWTSubject,接口要求我们实现两个方法:
getJWTIdentifier —— 返回模型的 id,一般直接使用 $this->getKey() 返回模型主键。
getJWTCustomClaims —— 返回数组,存放自定义的数据用于放在 Token 中,可以先返回空数组。
app/Models/UserModel.php
getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
这样就可以创建 Token 了,测试一下,打开 Tinker:
$user = User::find(1);
JWTAuth::fromUser($user);
找到 ID 为 1 的用户,使用 JWTAuth::fromUser 为这个用户创建一个 JWT。

可以看到这个很长的字符串就是一个 JWT 了,看一下它的结构,使用 base64_decode 可以解码这个字符串,或者我们直接去 jwt.io 解码看的更加清楚:

JWT 由头部(header)、载荷(payload)与签名(signature)组成,一个 JWT 类似下面这样:
{
"typ":"JWT",
"alg":"HS256"
}
{
"iss": "http://package.test",
"iat": 1536052439,
"exp": 1536056039,
"nbf": 1536052439,
"jti": "UIbnBVxa2K77MCMK",
"sub": 1,
"prv": "87e0af1ef9fd15812fdec97153a14e0b047546aa"
}
signature
- 头部申明了加密算法;
- 载荷中中记录了一些关键数据:
- iss:—— 签发者,也就是 package.test ;
- iat—— 签发时间;
- exp—— 过期时间;
- nbf —— 在这个时间之前,该 JWT 都是不可用的,一般同签发时间 iat;
- jti—— 唯一标识符,防止重放攻击。
- sub—— 用户标识,这里是用户 ID
- prv—— 扩展包自定义字段,模型名的哈希值,等于sha1(‘App\User’),用于区别不同的模型,下面的课程会深入介绍。
- 最后的 signature 是由服务器进行的签名,保证了 token 不被篡改。
JWT 最后是通过 Base64 编码的,也就是说,它可以被翻译回原来的样子来的。所以不要在 JWT 中存放一些敏感信息。
用户 id,过期时间等数据都保存在 Token 中了,所以并不需要将 Token 保存在服务器中,客户端请求的时候在 Header 中携带 Token,服务器获取 Token 后,进行 base64_decode 解码即可获取数据进行校验,由于已经有了签名,所以不用担心数据被篡改。
结合 Laravel Auth
一般我们希望通过 Laravel 的用户认证系统
Auth::guard来完成相关的功能,而不是直接使用扩展包提供的门面 JWTAuth,修改一下相关配置:
config/auth.php
guards => [
web => [
driver => session,
provider => users,
],
api => [
driver => jwt,
provider => users,
],
],
providers => [
users => [
driver => eloquent,
model => AppModelsUserModel::class,
],
],
将 api 的 driver 由 token 改为 jwt ,继续使用 tinker 测试一下,注意修改了代码要重启 tinker :
$credentials = [email => 1102389095@qq.com, password => secret];
auth(api)->attempt($credentials);
注意替换上面的 email 为你数据库中的邮箱,我们定义了一个 $credentials ,这个数组对应了请求提交过来的用户名以及密码,最后使用 attempt 来验证是否正确,验证成功会返回一个 JWT。
使用任意用户标识和用户密码,都可以作为验证参数。
完成接口
对于 API 来说一般需要以下几个接口:
- login —— 用户登录,获取 JWT;
- refresh—— 刷新 JWT;
- logout —— 退出登录,注销 JWT;
- user —— 获取当前 JWT 对应的用户。
当然你可能有自己的接口命名规范,我们这里只是讲解扩展包的使用,就直接使用扩展包文档中的命名了。这里可能会有疑惑的是 refresh 和 logout 两个接口,稍微解释一下:
-
刷新 JWT
任何一个永久有效的 token 都是相当危险的,通过任意方式泄露了 token 之后,用户的相关信息都有可能被利用。所以为了安全考虑,任何一种令牌的机制,都会有过期时间,过期时间一般也不会太长,可能几个小时或者几天。那么 token 过期以后,难道要用户重新登录吗?像 OAuth 2.0 有 refresh_token 可以用来刷新一个过期的 access_token,jwt-auth 同样也为我们提供了刷新的机制,只要在可刷新的时间范围内,即使 JWT 过期了,依然可以调用接口,换取一个新的 JWT。这对于客户端长期保持用户登录状态是十分重要的。我们需要了解两个时间- jwt.ttl (JWT_TTL) —— 多长时间以后 JWT 就过期了 (单位分钟);
- jwt.refresh_ttl (JWT_REFRESH_TTL) —— 多长时间以内, JWT 可以再次被刷新(单位分钟)。
一般情况下 refresh_ttl 应该大于 ttl,也就是 JWT 过期以后,依然可以刷新一个新的 JWT。
- 删除 JWT
- 用户退出登录的时候,是需要将当前这个 JWT 注销的,但是 JWT 本身不用存储在服务端,因为本身已经包含了足够的信息以及签名,那如何来完成注销呢?其实是利用了黑名单,删除只是将 JWT 加入黑名单(Laravel 缓存)而已,加入黑名单的 JWT 都是无法继续使用的。
routes/api.php
use IlluminateHttpRequest;
Route::post(login, function (Request $request) {
$credentials = $request->only(email, password);
if (!$token = auth(api)->attempt($credentials)) {
return response()->json([error => Unauthorized], 401);
}
return response()->json([token => $token]);
});
Route::post(refresh, function () {
return response()->json([token => auth(api)->refresh()]);
});
Route::post(logout, function () {
auth(api)->logout();
return response()->json(null, 204);
});
使用 PostMan 测试一下:
login 获取 JWT:

获取对应的用户,只需要将 JWT 放在 header 中,PostMan 可以填写在 Bearer Token 中:

刷新 JWT,注意刷新过之后,之前的 JWT 会被加入黑名单,也就不能继续使用了:

删除 JWT:

上面的代码应该很容易理解,你可以尝试一下优化一下,把方法写入 Controller,增加 Request 验证请求参数,返回合理的数据,等等。
多用户认证
创建 Admin
当我们的项目中需要为多个模型创建 Token,不同的 Token 可以使用不同的接口,这样的场景该如何处理呢?先来增加一个模型以及数据表。
php artisan make:model Admin -fm
-fm 参数是同时创建 migration 文件以及 factory 文件。

让 admins 与 users 拥有相同的字段:
database/migrations/< yourdate >createadminstable.php
increments(id);
$table->string(name);
$table->string(email)->unique();
$table->string(password);
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists(admins);
}
}
修改 Admin 模型:
app/Models/AdminModel.php
getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
database/factories/AdminFactory.php
define(AppAdmin::class, function (Faker $faker) {
return [
name => $faker->name,
email => $faker->unique()->safeEmail,
password => $2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm, // secret
remember_token => str_random(10),
];
});
执行 migrate:
php artisan migrate
快速创建两个用户:
factory(AppAdmin::class, 2)->create();

创建 / 验证 Token
修改 auth 配置:
config/auth.php
guards => [
...
admin => [
driver => jwt,
provider => admins,
],
],
providers => [
...
admins => [
driver => eloquent,
model => AppModelsAdminModel::class,
],
],
增加了一个 admin 的 guard,同时增加了对应的 provider。
测试一下为 admin 创建 JWT。
$admin = Admin::find(2);
auth(admin)->login($admin);

这次我们使用了 login 方法,与 fomUser 方法一样可以为某个用户创建一个 JWT,有兴趣的同学可以看看这两个方法的区别。
接下来我们可能就需要一份同 user 一样的接口登录以及获取信息的接口:
routes/api.php
Route::post(admin/login, function (Request $request) {
$credentials = $request->only(email, password);
if (!$token = auth(admin)->attempt($credentials)) {
return response()->json([error => Unauthorized], 401);
}
return response()->json([token => $token]);
});
Route::middleware(auth:admin)->get(/admin, function (Request $request) {
return $request->user();
});
可以正确的创建出来 JWT:

也可以正确的获取到对应 admin 用户的信息。

容易被忽略的问题
我们先分别为 User 和 Admin 生成 JWT ,对比一下:

你可能会有个疑问,JWT 是通过 sub 这个字段说明模型 ID 的,也仅仅是通过这个字段去查询对应的用户,也就是说上面生成的 $userToken 和 $adminToken 基本相同,那么是不是可以通过 $adminToken 去访问得到 User 的用户信息呢?
我们来尝试一下:

你会发现扩展包已经考虑到了这个问题, prv 字段用于记录扩展包的模型,相当于 $userToken 记录了 sha1(AppModelsUserModel) , $adminToken 记录了 sha1(AppModelsAdminModel) ,这样将不同模型的 JWT 进行隔离,不会出现问题。
需要注意的是,这个功能是在 1.0.0-rc.1 版本中才添加,对应的配置是 jwt.lock_subject 默认是 true。所以之前的版本确实会出现问题,我原来是通过模型中的 getJWTCustomClaims 方法,在 JWT 中存放一些额外的标识,然后自定义中间件来验证这个标识来解决这样的问题,不过将扩展包升级到最新之后就不用担心这个问题了,我们现在是 1.0.0-rc.2 版本。
并发问题 :
最后我们了解一个并发问题,JWT 在刷新了之后就会被加入黑名单,这样这个 JWT 就失效了。但是客户端有时候是并发请求的,也就是多个请求使用同一个 JWT 并发的请求各自的接口,但是如果某一个请求刷新了 JWT,那么其他所有的请求都会失败。
为了解决这个问题,扩展包提供了一个机制,可以配置多长时间内,JWT 被加入黑名单之后,依然可以使用,这个机制是用来防止并发问题,所以时间并不需要太长,具体的配置是 jwt.blacklist_grace_period ,可以在 env 中配置 JWT_BLACKLIST_GRACE_PERIOD。比如我们设置为 10,加入黑名单后 10 秒内依然可用。
创建一个可以正常使用的 JWT,

刷新这个 JWT,再次访问用户详情接口,依然可以获取到用户信息。但是等待 10 秒之后就会报错了。

版权声明
所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除
itfan123



