Laravel快速接入JWT用户认证(多用户认证)tymon/jwt-auth

2 篇文章 0 订阅

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="Tymon\JWTAuth\Providers\LaravelServiceProvider"

执行一下 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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class UserModel extends Authenticatable implements JWTSubject
{
    use HasFactory, Notifiable;

    protected $table = 'users';
    public $timestamps = false;

    public function getJWTIdentifier()
    {
        return $this->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' => App\Models\UserModel::class,
    ],
],

apidrivertoken 改为 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 Illuminate\Http\Request;

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

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAdminsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->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

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable implements JWTSubject
{
    use Notifiable;

    protected $table = 'admins';
    public $timestamps = false;

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

database/factories/AdminFactory.php

<?php

use Faker\Generator as Faker;

$factory->define(App\Admin::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(App\Admin::class, 2)->create();

在这里插入图片描述

创建 / 验证 Token
修改 auth 配置:

config/auth.php

'guards' => [
	...
    'admin' => [
        'driver' => 'jwt',
        'provider' => 'admins',
    ],
],
'providers' => [
	...
    'admins' => [
        'driver' => 'eloquent',
        'model' => App\Models\AdminModel::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('App\Models\UserModel')$adminToken 记录了 sha1('App\Models\AdminModel'),这样将不同模型的 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 秒之后就会报错了。

在这里插入图片描述

原文地址

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值