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


    JWT 是 JSON Web Token 的缩写,它是一个规范,让用户和服务器之间传递安全可靠的信息。

    创建新项目

    创建一个 Laravel的新项目,我们依然推荐大家使用 LTS 的版本:

    composer create-project --prefer-dist laravel/laravel laravel-test
    
    • 1
    • 配置站点并修改 host文件
    • 修改一下 env

    创建一下基础的数据表。

    php artisan migrate
    
    • 1

    这样项目就可以正常访问了。

    安装

    安装一下扩展包tymon/jwt-auth

    composer require tymon/jwt-auth
    
    • 1

    不发布配置也是可以使用的,可以直接通过 env 变量修改,为了方便之后的讲解,我们发布出出来。

    php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
    
    • 1

    执行一下 jwt:secret,这个命令会在 env 中增加一个 JWT_SECRET,同我们的 APP_KEY 这个 secret 是十分重要的,用于给 Token 签名,更换这个 secret 会导致之前生成的所有 Token 无效,所以不要随意的更换这个secret

    php artisan jwt:secret
    
    • 1

    快速接入

    创建 Token

    修改一下 User 模型,需要实现扩展包提供的接口 Tymon\JWTAuth\Contracts\JWTSubject,接口要求我们实现两个方法:

    getJWTIdentifier —— 返回模型的 id,一般直接使用 $this->getKey() 返回模型主键。
    getJWTCustomClaims —— 返回数组,存放自定义的数据用于放在 Token 中,可以先返回空数组。

    app/Models/UserModel.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 [];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这样就可以创建 Token 了,测试一下,打开 Tinker:

    $user = User::find(1);
    JWTAuth::fromUser($user);
    
    • 1
    • 2

    找到 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 头部申明了加密算法;
    • 载荷中中记录了一些关键数据:
      • 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,
        ],
    ],
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    apidrivertoken 改为 jwt,继续使用 tinker 测试一下,注意修改了代码要重启 tinker

    $credentials = ['email' => '1102389095@qq.com', 'password' => 'secret'];
    auth('api')->attempt($credentials);
    
    • 1
    • 2

    注意替换上面的 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);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用 PostMan 测试一下:

    login 获取 JWT:

    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述

    删除 JWT:

    在这里插入图片描述

    上面的代码应该很容易理解,你可以尝试一下优化一下,把方法写入 Controller,增加 Request 验证请求参数,返回合理的数据,等等。

    多用户认证

    创建 Admin

    当我们的项目中需要为多个模型创建 Token,不同的 Token 可以使用不同的接口,这样的场景该如何处理呢?先来增加一个模型以及数据表。

    php artisan make:model Admin -fm
    
    • 1

    -fm 参数是同时创建 migration 文件以及 factory 文件。

    在这里插入图片描述

    让 admins 与 users 拥有相同的字段:

    database/migrations/< yourdate >createadminstable.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');
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    修改 Admin 模型:

    app/Models/AdminModel.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 [];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    database/factories/AdminFactory.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),
        ];
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    执行 migrate:

    php artisan migrate
    
    • 1

    快速创建两个用户:

    factory(App\Admin::class, 2)->create();
    
    • 1

    在这里插入图片描述

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

    config/auth.php

    'guards' => [
    	...
        'admin' => [
            'driver' => 'jwt',
            'provider' => 'admins',
        ],
    ],
    'providers' => [
    	...
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\AdminModel::class,
        ],
    ],
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    增加了一个 admin 的 guard,同时增加了对应的 provider。

    测试一下为 admin 创建 JWT。

    $admin = Admin::find(2);
    auth('admin')->login($admin);
    
    • 1
    • 2

    在这里插入图片描述

    这次我们使用了 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();
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以正确的创建出来 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 秒之后就会报错了。

    在这里插入图片描述

    原文地址

  • 相关阅读:
    安装nacos配置jdk
    Flutter获取设备的信息
    Linux随记(四)
    英语语法汇总(9.时态)
    kubernetes调度
    QT下使用QTcpSocket与三菱FX5系列PLC通信实现
    数据分析案例-基于sklearn随机森林算法探究影响预期寿命的因素
    Asp.net MVC中文件夹中的控制器如何跳转到根目录的控制器中?
    基于体素场景的摄像机穿模处理
    【scala】阶段性练习
  • 原文地址:https://blog.csdn.net/Gan_1314/article/details/127795406