JWT 完整使用详解

2018/5/7

  • 说实话,官方文档也是相当的乱,这次根据文档并查看源码实验了很多地方,大改了一次。

2018/10/4

  • 结合大家提出的问题和我近期新的理解,写了一篇新文章,讲的比较深,大家可以去看看 JWT 超详细分析。本文这次主要修改了一些细节,对大家提出的问题整理写到文章中。

本文是以 1.0.0-rc 为基准的。

JWT 全称 JSON Web Tokens ,是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。它的两大使用场景是:认证和数据交换。

一、安装之前

资料

先摆出几个参考资料,可以把连接都打开,方便查阅:

二、安装及基础配置

Laravel

1. 使用 composer 安装

# 建议使用1.0以上版本
composer require tymon/jwt-auth 1.*@rc

2. 进行一些配置

这里值得注意的是,有些文档会说要添加 Tymon\JWTAuth\Providers\LaravelServiceProvider::class ,这只在 Laravel 5.4 及以下版本是必要的,更新的 Laravel 版本无需添加。

还有一些文档说要添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider 这是很久以前的 JWT 版本的(大概0.5.3 以前的版本)。

2.1 发布配置文件

# 这条命令会在 config 下增加一个 jwt.php 的配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

2.2 生成加密密钥

# 这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar
php artisan jwt:secret

2.3 更新你的模型

如果你使用默认的 User 表来生成 token,你需要在该模型下增加一段代码

<?php

namespace App;

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

class User extends Authenticatable implements JWTSubject    # 这里别忘了加
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * 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.4 注册两个 Facade

这两个 Facade 并不是必须的,但是使用它们会给你的代码编写带来一点便利。

config/app.php

'aliases' => [
        ...
        // 添加以下两行
        'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth',
        'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory',
],

如果你不使用这两个 Facade,你可以使用辅助函数 auth()

auth() 是一个辅助函数,返回一个guard,暂时可以看成 Auth Facade。

对于它有很多有必要说的,可以看我单独写的一篇文章——Laravel 辅助函数 auth 与 JWT 扩展详解

// 如果你不用 Facade,你可以这么写
auth('api')->refresh();
// 用 JWTAuth Facade
JWTAuth::parseToken()->refresh();

两个 Facede 常用可使用方法,可以看文章后面的附录。

2.5 修改 auth.php

config/auth.php

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',        // 原来是 token 改成jwt
        'provider' => 'users',
    ],
],

2.6 注册一些路由

注意:在 Laravel 下,route/api.php 中的路由默认都有前缀 api

Route::group([

    'prefix' => 'auth'

], function ($router) {

    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');

});

2.7 创建 token 控制器

php artisan make:controller AuthController

AuthController

值得注意的是 Laravel 这要用 auth('api') ,至于为什么,我另一篇关于 JWT 扩展详解的文章里有讲。

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     * 要求附带email和password(数据来源users表)
     * 
     * @return void
     */
    public function __construct()
    {
          // 这里额外注意了:官方文档样例中只除外了『login』
          // 这样的结果是,token 只能在有效期以内进行刷新,过期无法刷新
          // 如果把 refresh 也放进去,token 即使过期但仍在刷新期以内也可刷新
          // 不过刷新一次作废
        $this->middleware('auth:api', ['except' => ['login']]);
          // 另外关于上面的中间件,官方文档写的是『auth:api』
          // 但是我推荐用 『jwt.auth』,效果是一样的,但是有更加丰富的报错信息返回
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth('api')->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('api')->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     * 刷新token,如果开启黑名单,以前的token便会失效。
     * 值得注意的是用上面的getToken再获取一次Token并不算做刷新,两次获得的Token是并行的,即两个都可用。
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }
}

Lumen

1. 使用 composer 安装

上面是用命令行安装的,这里用 composer.json 安装。

// 我当时可用的版本是这个
"tymon/jwt-auth": "1.*@rc"

执行

composer update

2. 进行一些配置

2.1 开启 Facade 和 Eloquent

取消以下行的注释。

bootstrap/app.php

// $app->withFacades();

// $app->withEloquent();

2.2 开启中间件认证

取消以下行的注释。

bootstrap/app.php

// $app->routeMiddleware([
//     'auth' => App\Http\Middleware\Authenticate::class,
// ]);

// $app->register(App\Providers\AuthServiceProvider::class);

2.3 添加服务提供者

bootstrap/app.php

// 有些文档里是说添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,那是旧版本的
$app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);

2.4 生成加密密钥

这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar

php artisan jwt:secret

2.5 更新你的模型

如果你使用默认的 User 表来生成 token,你需要在该模型下增加一段代码

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
    use Authenticatable, Authorizable;

    ...

    /**
     * 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.6 注册两个 Facade

Lumen 中没有辅助函数 auth(),这两个 Facade 就挺有用了。

bootstrap/app.php

把原先去了注释的那一行再改一下。

$app->withFacades(true, [
    'Tymon\JWTAuth\Facades\JWTAuth' => 'JWTAuth',
    'Tymon\JWTAuth\Facades\JWTFactory' => 'JWTFactory',
]);

2.7 设置 auth.php

\vendor\laravel\lumen-framework\config\auth.php 也复制到 项目根目录config 文件夹(没有就新建)。

文档中有提到 Lumen 风格的配置文件这个概念

指的就是都在 .env 文件中设置各种设置项,在 \vendor\laravel\lumen-framework\config 文件夹下面的其他配置文件中也可以看到,很多配置项都有 env(设置项key, 默认值) 这个方法,有这个配置项的就可以在 .env 文件中设置 设置项=你的设置值 这样设置。

而复制到根目录 config 文件夹是 Laravel 风格的配置文件实现方式

这里我本来想尽量按 Lumen 风格实现的,但是下面这些属性默认并没有 env() 方式实现,所以我还是复制到根目录下改算了。

auth.php

按下面进行添加或修改。

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

...

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => \App\User::class
]

2.8 注册一些路由

Route::group([

    'prefix' => 'auth'

], function ($router) {

    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');

});

2.9 创建 token 控制器

Lumen 还精简了很多辅助函数,比如 auth 和 bcrypt 等。

可以安装 cosmicvelocity/lumen-helpersalbertcht/lumen-helpers 补全(建议用后者,更好安装)

或者使用上面说的两个 Facade。

AuthController.php

如果你没使用扩展补充的辅助函数,你需要这么写,不然直接用上面的 Laravel 那个

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        // 这里额外注意了:官方文档样例中只除外了『login』
          // 这样的结果是,token 只能在有效期以内进行刷新,过期无法刷新
          // 如果把 refresh 也放进去,token 即使过期但仍在刷新期以内也可刷新
          // 不过刷新一次作废
        $this->middleware('auth:api', ['except' => ['login']]);
          // 另外关于上面的中间件,官方文档写的是『auth:api』
          // 但是我推荐用 『jwt.auth』,效果是一样的,但是有更加丰富的报错信息返回
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (! $token = JWTAuth::attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(JWTAuth::parseToken()->touser());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        JWTAuth::parseToken()->invalidate();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(JWTAuth::parseToken()->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => JWTAuth::factory()->getTTL() * 60
        ]);
    }
}

关于中间件

1.0 版本以上的 jwt-auth,中间件在服务提供者中已经定义了,所以不需要额外写,按上面来即可。

下面是可用的中间件,第一二个功能一样,但是第二个不会抛出错误,第三四个功能一样,没什么区别。

tymon\jwt-auth\src\Providers\AbstractServiceProvider.php

protected $middlewareAliases = [
    'jwt.auth' => Authenticate::class,
    'jwt.check' => Check::class,
    'jwt.refresh' => RefreshToken::class,
    'jwt.renew' => AuthenticateAndRenew::class,
];

三、JWT Token 详解

1. token 的获取、使用、删除和刷新

  • 以下用 postman 演示,{{TEST}} 为 postman 全局变量:test.yfree.ccc
  • Laravel 环境下写在 api.php 中的路由默认有前缀 api
  • 下面的图是 Lumen 环境的,没有默认区前缀 api

1.1 获取 token

获取token

JWT 完整使用详解

1.2 使用 token

有两种使用方法:

  • 加到 url 中:?token=你的token
  • 加到 header 中,建议用这种,因为在 https 情况下更安全:Authorization:Bearer 你的token

使用 token
JWT 完整使用详解

添加中间件保护的就需要使用 token进行访问

可以使用的中间件有 auth、auth:api、jwt.auth、jwt.refresh、jwt.check、jwt.renew

关于这些中间件之间有什么差别,可以看我的另一篇文章:Laravel 辅助函数 auth 与 JWT 扩展详解

1.3 删除 token

删除 token
JWT 完整使用详解

删除 token 后,token就会失效,无法再利用其获取数据。

1.4 刷新 token

刷新 token
JWT 完整使用详解

刷新后,旧 token 将会失效,但是你可以设置一个宽限时间,这个在后面具体说。

2. token 的组成、创建以及解析

2.1 组成

一个 JWT token 是一个字符串,它由三部分组成,头部、载荷与签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

头部(header)

头部通常由两部分组成:令牌的类型(即JWT)和正在使用的签名算法(如HMAC SHA256 或 RSA.)。
例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后用 Base64Url 编码得到头部,即 xxxxx

载荷(Payload)

载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换。

载荷的属性也分三类:

  • 预定义(Registered)
  • 公有(public)
  • 私有(private)

预定义的载荷

{
  "sub": "1",
  "iss": "http://localhost:8000/auth/login",
  "iat": 1451888119,
  "exp": 1454516119,
  "nbf": 1451888119,
  "jti": "37c107e4609ddbcc9c096ea5ee76c667",
  "aud": "dev"
}

这里面的前 7 个字段都是由官方所定义的,也就是预定义(Registered claims)的,并不都是必需的。

  • iss (issuer):签发人
  • sub (subject):主题
  • aud (audience):受众
  • exp (expiration time):过期时间
  • nbf (Not Before):生效时间,在此之前是无效的
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

公有的载荷

在使用 JWT 时可以额外定义的载荷。为了避免冲突,应该使用 IANA JSON Web Token Registry 中定义好的,或者给额外载荷加上类似命名空间的唯一标识。

私有载荷

在信息交互的双方之间约定好的,既不是预定义载荷也不是公有载荷的一类载荷。这一类载荷可能会发生冲突,所以应该谨慎使用。

将上面的 json 进行 Base64Url 编码得到载荷,,即 yyyyy

关于载荷的理解:

这里三种载荷的定义应该明确的一点是 —— 对于后两种载荷,它并非定义了载荷的种类,然后让你去选用哪种载荷,而是对你可能会定义出来的载荷做一个分类。

比如你定义了一个 admin 载荷,这个载荷按其分类应该是私有载荷,可能会和其他人定义的发生冲突。但如果你加了一个前缀(命名空间),如 namespace-admin,那么这应该就算一个公有载荷了。(但其实标准并没有定义怎么去声明命名空间,所以严格来说,还是可能会冲突)

但是在现实中,团队都是约定好的了要使用的载荷,这样的话,好像根本不存在冲突的可能。那为什么文档要这么定义呢?我的理解是,RFC 是提出一种技术规范,出发点是一套通用的规范,考虑的范围是所有开发者,而不仅仅局限于一个开发者团队。就像用 token 做认证已经是很常见的技术了,但是 JWT 的提出就相当于提出了一套较为通用的技术规范。既然是为了通用,那么考虑在大环境下的冲突可能性也是必须的。

签名(Signature)

签名时需要用到前面编码过的两个字符串,如果以 HMACSHA256 加密,就如下:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz

组合便可以得到 token:xxxxx.yyyyy.zzzzz

签名的作用:保证 JWT 没有被篡改过,原理如下:

HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

Hash-based Message Authentication Code

PHP 代码示例

// 这里要开启true
$zzzzz = $this->base64url_encode(hash_hmac('sha256', 'xxxxx.yyyyy', getenv('JWT_SECRET'), true));

protected function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

2.2 token 的创建

前面的 AuthController.php 中有两行展现了这一种 token 的创建方法,即用用户所给的账号和密码进行尝试,密码正确则用对应的 User 信息返回一个 token

token 的创建方法不止这一种,接下来介绍 token 的三种创建方法:

  • 基于账密参数
  • 基于 users 模型返回的实例
  • 基于 users 模型中的用户主键 id

a) 基于账密参数

这就是刚刚说的哪一种,贴出具体代码。

// 使用辅助函数
$credentials = request(['email', 'password']); 
$token = auth()->attempt($credentials)

// 使用 Facade
$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials);

b) 基于 users 模型返回的实例

// 使用辅助函数
$user = User::first();
$token = auth()->login($user);

// 使用 Facade
$user = User::first();
$token = JWTAuth::fromUser($credentials);

c) 基于 users 模型中的主键 id

// 使用辅助函数
$token = auth()->tokenById(1);

// 使用 Facade
源码中没找到

2.3 token 的解析

a) 解析 token 到对象

只有 Facade 需要这样。

// 把请求发送过来的直接解析到对象
JWTAuth::parseToken();

b) 获取 token 中的 user 信息

// 辅助函数
$user = auth()->user();

// Facade
$user = JWTAuth::parseToken()->authenticate();

c) 获取 token

如果 token 被设置则会返回,否则会尝试使用方法从请求中解析 token ,如果token未被设置或不能解析最终返回false。

// 辅助函数
$token = auth()->getToken();

// Facade
$token = JWTAuth::parseToken()->getToken();

更多方法可以看文章后面的附录。

d) 如果是前端

直接 base64 解码 token 的前两段即可以知道所需的信息。

3. 载荷的设置和获取

a) 载荷设置

载荷信息会在 token 解码时得到,同时越大的数组会生成越长的 token ,所以不建议放太多的数据。同时因为载荷是用 Base64Url 编码,所以相当于明文,因此绝对不能放密码等敏感信息。

$customClaims = ['foo' => 'bar', 'baz' => 'bob'];

// 辅助函数
$token = auth()->claims($customClaims)->attempt($credentials);

// Facade - 1
$token = JWTAuth::claims($customClaims)->attempt($credentials);

--- 下面两种试了好像不行,不过前面的够用了

// Facade - 2
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);

// Facade - 3
$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar' => 'baz'])->make();
$token = JWTAuth::encode($payload);

b) 载荷解析

从请求中把载荷解析出来。可以去看扩展源代码,里面还有很多的方法。

// 辅助函数
$exp = auth()->payload()->get('exp');
$json = auth()->payload()->toJson();
$array = auth()->payload()->jsonSerialize();
$sub = $array['sub'];

// Facade - 1
$payload = JWTAuth::parseToken()->getPayload();
$payload->get('sub'); // = 123
$payload['jti']; // = 'asfe4fq434asdf'
$payload('exp') // = 123456
$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4fq434asdf'] etc

// Facade - 2
$exp = JWTAuth::parseToken()->getClaim('exp');

4. token 的三个时间

一个 token 一般来说有三个时间属性,其配置都在 config/jwt.php 内。

有效时间

有效时间指的的是你获得 token 后,在多少时间内可以凭这个 token 去获取内容,逾时无效。

// 单位:分钟
'ttl' => env('JWT_TTL', 60)

刷新时间

刷新时间指的是在这个时间内可以凭旧 token 换取一个新 token。例如 token 有效时间为 60 分钟,刷新时间为 20160 分钟,在 60 分钟内可以通过这个 token 获取新 token,但是超过 60 分钟是不可以的,然后你可以一直循环获取,直到总时间超过 20160 分钟,不能再获取。

这里过期后能否刷新,经 [@Rootrl](https://learnku.com/users/433) 指出,其实并不是这么绝对,具体细节,看我们上面 AuthController 处的代码。有详细补充
这也是一个 token 被加入黑名单之后,会存在的时间

// 单位:分钟
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160)

宽限时间

宽限时间是为了解决并发请求的问题,假如宽限时间为 0s ,那么在新旧 token 交接的时候,并发请求就会出错,所以需要设定一个宽限时间,在宽限时间内,旧 token 仍然能够正常使用。

// 宽限时间需要开启黑名单(默认是开启的),黑名单保证过期token不可再用,最好打开
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true)

// 设定宽限时间,单位:秒
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60)

5. 关于 JWT 的讨论

5.1 为什么用 JWT?

看我的新文章 JWT 超详细分析

5.2 token 的刷新问题?

a) token 为什么要刷新吗?

首先 Basic Auth 是一种最简单的认证方法,但是由于每次请求都带用户名和密码,频繁的传输肯定不安全,所以才有 cookiessession 的运用。如果 token 不刷新,那么 token 就相当于上面的用户名+密码,只要获取到了,就可以一直盗用,因此 token 设置有效期并能够进行刷新是必要的。

b) token 有效期多久合适,刷新频率多久合适?

有效期越长,风险性越高,有效性越短,刷新频率越高,刷新就会存在刷新开销,所以这需要综合考虑。而且 web 端应该设置为分钟级和小时级,而移动端应该设置为天级和周级。

c) 有没有必要每次都刷新 token ?

看我的新文章 JWT 超详细分析

四、附录

1. JWT 的 两个 Facade

1.1 JWTAuth

JWTAuth::parseToken()->方法() 一般都可以换成 auth()->方法()

token 生成

attempt

根据 user 账密新建一个 token。

$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials)

fromUser or fromSubject

根据 user 对象生成一个 token。后者是前者别名。

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

token 控制

refresh

更新 token。

$newToken = JWTAuth::parseToken()->refresh();

invalidate

让一个 token 无效。

JWTAuth::parseToken()->invalidate();

check

检验 token 的有效性。

if(JWTAuth::parseToken()->check()) {
    dd("token是有效的");
}

token 解析

authenticate or toUser or user

这三个效果是一样的,toUserauthenticate 的别名,而 user 比前两者少一个 user id 的校验,但并没有什么影响。

$user = JWTAuth::parseToken()->toUser();

parseToken

从 request 中解析 token 到对象中,以便进行下一步操作。

JWTAuth::parseToken();

getToken

从 request 中获取token。

$token = JWTAuth::getToken();  // 这个不用 parseToken ,因为方法内部会自动执行一次

载荷控制

customClaims or claims

设置载荷的 customClaims 部分。后者是前者的别名。

$customClaims = ['sid' => $sid, 'code' => $code];
$credentials = $request->only('email', 'password');
$token = JWTAuth::customClaims($customClaims)->attempt($credentials);

getCustomClaims

获取载荷的 customClaims 部分,返回一个数组。

$customClaims = JWTAuth::parseToken()->getCustomClaims()

getPayload or payload

获取所有载荷,三个都是一样的,最后一个一般用来检验 token 的有效性

$payload = JWTAuth::parseToken()->payload();

// then you can access the claims directly e.g.
$payload->get('sub'); // = 123
$payload['jti']; // = 'asfe4fq434asdf'
$payload('exp') // = 123456
$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4fq434asdf'] etc

getClaim

获取载荷中指定的一个元素。

$sub = JWTAuth::parseToken()->getClaim('sub');

1.2 JWTGuard

这个 Facade 主要进行载荷的管理,返回一个载荷对象,然后可以通过 JWTAuth 来对其生成一个 token。

// 载荷的高度自定义
$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar' => 'baz'])->make();
$token = JWTAuth::encode($payload);
$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);

1.3 其他一些用法

这里用 auth 的写法,因为 Laravel 有多个 guard,默认 guard 也不是 api ,所以需要写成 auth('api') 否则,auth() 即可。

设置载荷

$token = auth('api')->claims(['foo' => 'bar'])->attempt($credentials);

显示设置 token

$user = auth('api')->setToken('eyJhb...')->user();

显示设置请求

$user = auth('api')->setRequest($request)->user();

重写有效时间

$token = auth('api')->setTTL(7200)->attempt($credentials);

验证账密是否正确

$boolean = auth('api')->validate($credentials);
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 103

每一步都是实验过得出的,精简到了最少步骤,一步一步照着来应该是没有任何问题的。

如果有什么不对的地方可以在评论下回复。

2年前 评论

每一步都是实验过得出的,精简到了最少步骤,一步一步照着来应该是没有任何问题的。

如果有什么不对的地方可以在评论下回复。

2年前 评论

@Hatcher 有空我修一下,其实也不是什么特别重要的图 :joy:

2年前 评论

确实,之前也是踩了不少坑,但是一直没时间分享,给你打Call

2年前 评论

我就是被官方文档坑过,死活按照文档没走通的那个人。。感谢LZ,我会试试你这个,到时候如果有问题再来麻烦你

2年前 评论

有空讲一下客户端如果保护 secret 呢~

2年前 评论

/api/auth/login

2年前 评论

多用户表认证,有没有什么好办法呢?

2年前 评论

@gaodevops 的确,Laravel 环境下写在 api.php 中的路由默认有前缀 api,虽然官方文档有讲,但我也加一句。

2年前 评论

@Insua 配置文件 auth.php 里面可以为每个看守器配置 user 来源表,我猜应该可以从那下手,具体没试过。

2年前 评论

也想知道多表用户如何使用,3个月之前用了但是最新的版本的配置了每个不同model的看守器。虽然可以验证通过,但是实际测试,A表拿到的token,可以过B表的校验。然后继而放弃tymon/jwt-auth了 转向了 passport

2年前 评论

有一处在我这里有问题:
JWTAuth::parseToken->invalidate();

我这的 laravel 5.5 要写成:JWTAuth::parseToken()->invalidate(); 才正确,

另外新人请教两个问题:

1)中间件 auth:api,如果没有通过是跳转到登录页面,我要做成返回json代码应该怎么做?
2)调用刷新接口的时候让之前的token失效应该怎么做?

2年前 评论

的确,是我写漏了,已改正。

问题1:两种办法

  1. X-Requested-With:XMLHttpRequest 添加这个 header ,框架就会当初 ajax 请求,就会返回 json
  2. 安装 dingo ,统一设置返回格式,只要 throw 错误的地方都会以你设置的 json 格式返回

问题2:
刷新后,显式指定旧 token,然后 logout

2年前 评论
oyghan

写的很详细,收益。给楼主点赞 :+1:

2年前 评论

请问如果不用默认的 user 表该怎么样修改,一直没跑通

2年前 评论
AllmeMineUs 5个月前

@skyArony

发现有个问题,无论是logout() 方法,还是手动退出 auth('api')->logout() 这样,都无法退出

2年前 评论

@大师兄 可以用 invalidate ,你那不生效可以看看具体代码吗?

2年前 评论

@skyArony 用invalidate也没有用, 源码我传到github上了:http://t.cn/R3T3MuU

2年前 评论

@sunlinesun 使用token做身份识别

2年前 评论

写的非常友好,很详细

2年前 评论

一股清流,十分赞

2年前 评论

@skyArony 我这边调用退出有些问题,只显示了退出页面,没显示json

2年前 评论

@skyArony
如何兼容url带token?

2年前 评论

可以稍微讲下前端登录和刷新的流程吗?

我现在用的是社区另一个人的方案,就是中间件里,如果token过期,自动刷新。。。

2年前 评论

@158abcd1510 专栏里还有另一篇文章,可以看看

2年前 评论
ChengFu

那怎么在路由 做拦截呢 就是 没有带token的话 就报错

2年前 评论

写的非常详细,感谢分享哈。

2年前 评论

这是看过的最清晰的一篇教程了,点赞 :+1::+1::+1::+1::+1::+1::+1:

1年前 评论

感谢引入我的公众号 (coding01)的文章
学习 Lumen 用户认证 (一)
https://mp.weixin.qq.com/s/KVUQE2DUetNB2kq...

1年前 评论
dividez

file

token的有效期是 60分钟,60分钟过期后, 通过下面的代码进行刷新

$newToken = JWTAuth::parseToken()->refresh();

提示token 已经失效,

file

楼主这里说的一直循环是什么意思?token 已经过期了,循环是做什么操作呢?

1年前 评论

@dividez 这里可能没讲清楚,我解释一下:

『循环获取』的意思是,用 A 换到 新 token B,再用 B 可以换到 新 token C,然后 C 又可以换到 新 token D。每次更新,旧 token 就会失效,当然这里可以设置一个宽限时间让 A 换到 B 后不至于马上失效。

1年前 评论
ThinkCsly

谢谢,很好。

1年前 评论
刷新时间
刷新时间指的是在这个时间内可以凭旧 token 换取一个新 token。例如 token 有效时间为 60 分钟,刷新时间为 20160 分钟,在 60 分钟内可以通过这个 token 获取新 token,但是超过 60 分钟是不可以的,然后你可以一直循环获取,直到总时间超过 20160 分钟,不能再获取。

楼主这里好像讲错了? “在 60 分钟内可以通过这个 token 获取新 token,但是超过 60 分钟是不可以的” 过了60分钟 但是在20160内是可以再次刷新的吧

1年前 评论
假老练 4个月前

设置token不过期需要怎么设置

1年前 评论

@rootrl 并非如此,JWT token(在这个组件中的实现)是超过了60分钟是无法用来刷新以获取新 token 的,也就是说 token 之间的生命必须是相连的。你理解的这个,在 Oauth2.0中的 refresh_token 上到的确是这么回事。
后续我会把 token 刷新那一块详细补充一下。

1年前 评论

我的版本是:"tymon/jwt-auth": "^1.0.0-rc.1",我测试结果是我说的这样的。而且如果过了60min不能刷新,那么就不存在过期刷新这个概念了呀,就只是个纯粹的时间限制了。 比如中间件一般是捕获过期异常,然后尝试刷新。如果过期即刷新那就没这个逻辑了。总之,我现在测得是可以刷新的。不知道规范是怎么设计的?

1年前 评论

@rootrl 我刚刚也在测,打算统一整理一下。你有设置宽限时间吗?我这如果宽限时间设置为0,就是我说的那样的效果,但是宽限时间如果设置为 0 以上就可以刷新,不过这时还要考虑是自然过期还是手动刷新导致的过期。现在还没有测试的很清楚,待会我整理好了会添加到文章上。

1年前 评论

@skyArony 设置了1min宽限时间的。好的,你可以把详细结果整理下。其实你这篇文章写的很详细的。感谢!

1年前 评论

@rootrl 刚好公司分享就在整理这个,又给我发现了很多 jwt 的小问题。关于这个刷新,主要整理了以下几点:

  1. 官方文档的 AuthControlller 中只把 login 排除在了检测中间件之外,但这样有问题:

    • 宽限时间设置为 0 的话,token 一旦过有效期,就永远无效了,不能进行任何操作,包括刷新。
    • 宽限时间设置为 0 以上的话,token 一旦过有效期,就永远无效了,不能进行任何操作,包括刷新,可是如果有效期以内进行刷新或注销,都会把 token 的有效期设置为宽限时间,但是不会超过其有效期。
  2. 应该把 refresh 操作也排除在检测中间件之外,这样更复合业务逻辑,并且这样可以在 token 过期后但是刷新期以内刷新 token
    public function __construct() {
    $this->middleware('jwt.auth', ['except' => ['login', 'refresh']]);
    }

    还有蛮多细节不好在这打出来,过几天统一添加到文章上。

1年前 评论

@skyArony 总之,这个扩展还是挺好用的。我们现在有个接口分商家和用户两套体系,然后token机制都是基于这个实现。目前运行没什么问题,能达到我想要的结果,包括在header中自动刷新Token。期待你的更新。^_^

1年前 评论

@Rootrl 已更新哈,写了一篇新文章,可以看看,如果有错误,欢迎指出。

1年前 评论

@skyArony @Rootrl 请教一下,这个token 过期的异常该怎么捕捉到?个人尝试了是失败。

file

这里不能生效,结果都是 (如图)

file

1年前 评论

@god-lin 要捕获第一个图的异常得用 jwt.auth 这个中间件,至于你这个报错,没具体代码,不太清楚啥原因,检查检查路由试试。

1年前 评论

@erigo 你这个可能原因是你访问的api/auth/me路由需要鉴权,但是你现在是没带授权信息就直接访问此路由 而auth('api')这套权限机制走的是以前http页面那套,准备把你跳到login页面,先登录授权。但是呢,你又没有这个login路由页面,所以抛出此异常。

解决也很简单,首先你要理清你的业务逻辑。你可以在app/Exceptions/Handler.php 的render方法中捕获UnauthorizedHttpException这个异常,在此处直接返回鉴权失败的json格式信息给客户端。

另外就是token过期异常其实是自己定义个中间件捕获,然后此中间件还可以兼带刷新token功能,我这里直接命名RefreshToken。

1年前 评论

@skyArony 好的 才看到 向你学习^_^ 我国庆也写了篇思路文章:https://rootrl.github.io/2018/10/03/%E5%85... 欢迎指正

1年前 评论

@skyArony @Rootrl
现在是使用了 jwt.auth,得到了具体的 报错,也就是令牌失效,结果如图
file
但是并没有达到我要的目的,我这里是一个接口,不是页面跳转。(因为要实现前后端分离)现在已经可以生成令牌,进行令牌刷新,和通过令牌去进行 用户信息的返回,只是当令牌失效时,会出现异常。现在是想通过捕获或者获取到这个异常,然后接口返回 需要重新登录 的消息返回。

  1. 控制器更改 auth:api,使用 jwt.auth (可以获取到更详细的 原因:令牌失效)
    file
  2. 通过 token 获取用户相关的信息,(token 不过期是可以 正常返回 用户信息 )路由设置和对应的方法代码

file

file

  • 应该是路由检查方法时,在这里 throw 了异常,然后就是这里的异常怎么自己获取到,然后在自己的接口返回具体的消息

file
就是这样了,这里的异常可以通过什么方法自己获取到,不要直接 报 这样的错误 吗

file

1年前 评论

@god-lin 不好意思 我回答你了 只是at错了人。。你仔细看我给你的回答 上面有捕获异常方法

你就你上面第二条

1年前 评论
snail404

我测试的结果也是,token 过期,在刷新时间为 20160 分钟内,也是可以刷新的,与设置宽限时间无关,我的理解是,中间件,检测token 失效,失效后刷新token 捕捉是否抛出异常,没有就在刷新期内,就正常使用,我现在测试有个问题就是可以刷新,旧的token 在刷新后也正常加入,但是这个被加入黑名单后的token,无法进行后面的验证操作,我设置了宽限时间,应该是在宽限时间内是可以正常使用的吧?@skyArony @Rootrl

1年前 评论

@snail404 中间件检测失效,失效就刷新重新返回token,此时客户端应该有个检测服务端是否返回token的逻辑,有就刷新本地token。走这套模式,不存在旧的token问题。

1年前 评论
snail404

@Rootrl 在调用接口操作是,我这个token 失效但还在刷新时间内,应该可用,然后会继续下面得操作,响应同时会附带新得token

1年前 评论
snail404

@Rootrl 这样减少一次得请求操作,我理解得逻辑应该是这样,然后那个黑名单得宽限时间,就允许接下来得正常操作(因为在刷新得同时将旧得加入黑名单),就不确定是不是我理解问题,还是代码使用问题

1年前 评论

@snail404 是这样呀,本次的请求能顺利完成,比如黑名单一般有一分钟左右期限,这期中都有效的。

1年前 评论
snail404

@Rootrl 我现在每次刷新的时候,下面的操作token 就不可用,提示 The token has been blacklisted,黑名单设置跟时间是有开启的,下一次的请求也是校验不通过的,你有没有遇到过这个问题?

1年前 评论
gengwenlong 8个月前
www2423432 3个月前
LuckyUser 1个月前

@snail404 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60), 这两项是这样么?

1年前 评论
snail404

@Rootrl 对,是这样的

1年前 评论

@snail404 那就不清楚了,我这边能达到我要的效果。你调试下,在程序中打印下这配置。

1年前 评论
snail404

好的,多谢

1年前 评论

请问如果auth失效了
php 'middleware' => ['auth:api']
加上这个中间件后前端就没办法取到这个http错误码401了吗

1年前 评论

@monch 这个会返回 500

1年前 评论

完全按照教程上操作,laravel5.6和5.7 都提示没有respondWithToken 这个方法. 请问原因.

1年前 评论

如何修改验证字段不为email

use AuthenticatesUsers;
$credentials = request(['uname', 'password']);
    public function username()
{
    return 'uname';
}
1年前 评论

auth('api')->user() 会返回模型的所有属性,有没有办法可以控制让它只返回自己需要的属性,比如我只需要id,username,其他的属性不需要返回,要怎么处理

1年前 评论

Laravel 辅助函数 auth 与 JWT 扩展详解 这个链接失效了!

1年前 评论

Laravel 辅助函数 auth 与 JWT 扩展详解 楼主能补一下链接吗,谢谢了。很想看!

1年前 评论

@muqi001 补上了,其实在我专栏里就可以找到

1年前 评论

@skyArony 您好!请教个问题,如果我整站用户角色比较多,采用前后端分离的方式来开发,是否可以将前后台所有用户的基础信息(用户名、密码)放置在一个表(users)中做jwt的认证呢?而非采用多个guard的方式?

1年前 评论

@god-lin 不知是否已经解决,但是可以提供一个其他的思路。那个报错其实是当token失效或无效时帮你自动跳转到了被命名为 login 的路由当中。但是如果写接口或新创建的项目默认是没有这个路由的,所以就报错了。
你完全可以直接创建一个针对此异常返回的一个路由然后命名为 login 。这样每次遇到这种情况就会帮你跳转到你的login路由了。

    Route::get('/unauthorized', function () {
        return response()->json(['error' => 'Unauthorized'], 401);
    })->name('login');
1年前 评论

@Echoiii dd(auth('api')->user()) 可以看出这个返回的是User模型。所以对应User模型和集合的方法是都可以用的。比如只想要 name 就可以 auth('api')->user()->only(['name']) 还有一些User模型的资源修改的方法都可以尝试下。

1年前 评论

@大师兄 JWTAuth::invalidate(JWTAuth::getToken());

1年前 评论

赞,学到了,不过laravel5.5里的kernel的$routeMiddleware是这样注册的,但是编辑器提示不对。。。
'jwt.auth' => \Tymon\JWTAuth\Middleware\GetUserFromToken::class,
'jwt.refresh' => \Tymon\JWTAuth\Middleware\RefreshToken::class,

1年前 评论

@键盘侠 看清楚、是博主自定义的方法

1年前 评论

密码的加密方式能修改吗?如果能修改应该如何操作?

1年前 评论

请问一下 public function login()
{
$credentials = request(['email', 'password']);
if (! $token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}

        return $this->respondWithToken($token);
}

这一步的 的输出结果总是 Unauthorized 这个验证用的什么来验证email和password

1年前 评论
isrocky 11个月前

楼主你好~我在RefreshToken中间件中,仿照官网使用try:$result = auth('api')->authenticate(),然后catch过期token的TokenExpiredException,但是token虽然过期了但貌似并没有抛出这个异常,我现在无法捕获到token过期的异常,请问应该怎样做?谢谢了

1年前 评论
13697332484 6个月前

如何更换验证?数据库中没有email和password,想要换成user_phone和user_password

1年前 评论

能不能有个详细的,token 过期刷新的中间件详解?
如何返回新的token?修改header
现在不知道怎么弄这个中间件,jwt.refresh','jwt.renew' 这两个中间件一般在说明时候用?

1年前 评论

微信 浏览器 auth('api')->user() 返回null 什么原因

1年前 评论

文章中好像没有提到,做个记号,想要根据一个token串获取用户是否已认证,可以使用:

$ok = JWTAuth::setToken('your_token_string')->check(); // true or false
1年前 评论
Natural

我想问下用 Facade 怎么指定 guards?

解决办法: 在构造方法中 动态修改默认的guards

    public function __construct()
    {
        config(['auth.defaults.guard' => 'admin','auth.defaults.passwords' => 'admin']);
    }
1年前 评论
duke

不用默认的user模型,用laravel-admin的admin_users报错,Argument 1 passed to Tymon\JWTAuth\JWT::fromUser() must be an instance of Tymon\JWTAuth\Contracts\JWTSubject, instance of Encore\Admin\Auth\Database\Administrator given, called in /Applications/MAMP/htdocs/baccarat-manage/vendor/tymon/jwt-auth/src/JWTAuth.php on line 54

1年前 评论
13697332484 6个月前

中间件不管使用auth:api还是jwt.auth,认证的时候都是报"Method Illuminate\Auth\RequestGuard::onceUsingId does not exist.",lumen 5.8 + jwt 1.4有遇到过的吗?看了源码,不太理解,才发问求解决。

1年前 评论
Natural

lumen 5.8 之前验证还可以得 现在不可以了

1年前 评论

因为数据库加密的方式是md5的,不知道在登录验证api/auth/login 这个地方去验证的时候密码是什么加密方式;这个地方的加密方式需要一致吗?
$credentials = request(['email', 'password']);
auth('api')->attempt($credentials)

file

1年前 评论
isrocky 10个月前
1164880236 (作者) 10个月前
mantou_1 8个月前

请问下,invalidate() 方法是否是使 token 临时失效吗?我使用一个 token ,第一次请求时,使它失效,然后第二次还是用这个 token,这个 token 依然有效。

file

1年前 评论

file
请问下lumen框架在这个步骤里 无法生成token 一般是由哪些地方引起的呢

11个月前 评论
isrocky 11个月前

用户登录后怎么把之前的token删除

11个月前 评论

看来大家都有类似的问题

10个月前 评论

请问下,lumen 安装jwt之后 php artisan list 找不到jwt相关的命令,这是怎么回事?lumen版本5.8.12,jwt 版本1.0.0-rc.4.1

重新安装之后解决了。

10个月前 评论

这个jwt可以跟java的一起用吗?

8个月前 评论

请问一下如何使用自己的模型来构建?

file

8个月前 评论

刷新时间那里我觉得没说清楚

刷新时间指的是在这个时间内可以凭旧 token 换取一个新 token。例如 token 有效时间为 60 分钟,刷新时间为 20160 分钟,在 60 分钟内可以通过这个 token 获取新 token,但是超过 60 分钟是不可以的,然后你可以一直循环获取,直到总时间超过 20160 分钟,不能再获取。

我觉得应该这样解释:
刷新时间指的是在这个时间内可以凭旧 token 换取一个新 token。例如 token 有效时间为 60 分钟,刷新时间为 20160 分钟。代表你的token在60分钟内可以正常使用,超过60分钟则过期无法使用。但在20160 分钟的期限内你可以在任意时刻凭旧 token 换取新 token。

6个月前 评论

b) 基于 users 模型返回的实例#
// 使用辅助函数
$user = User::first();
$token = auth()->login($user);

// 使用 Facade
$user = User::first();
$token = JWTAuth::fromUser($credentials);


这里的$credentials是不是应该是$user

4个月前 评论

我想问问大家,那没通过验证的呢?比如用户未携带token或者token已过期访问的情况下?在哪报错?框架默认是直接让跳转到login路由的,你们都怎么处理的?

4个月前 评论

这里有个坑,使用辅助函数获取用户token会报错

  // 使用辅助函数
  $token = auth()->tokenById(1);
  //如果在config/auth.php中guard没有设置默认的defaults使用方法应该为:$token = auth('api)->tokenById(1);

报错:

  Argument 1 passed to Tymon\\JWTAuth\\JWT::fromUser() must implement interface Tymon\\JWTAuth\\Contracts\\JWTSubject, instance of App\\User given, called in /home/homestead/code/yunto/vendor/tymon/jwt-auth/src/JWTGuard.php on line 198",

方法中并未使用fromUser方法,直接用fromUser(User对象)登录没有问题,我的用户模型为App\User\Models,也实现了JWTSubject接口。
然后找问题,找到在app目录下有一个User.php(使用了laravel-admin),直接删除也会报错,通过全局搜索发现在:

  admin\config\services.php
  admin\app\Http\Controllers\Auth\RegisterController.php
  admin\config\auth.php(已经配置过的可以忽略)
  admin\app\Http\Controllers\Auth\RegisterController.php
  admin\database\factories\UserFactory.php

这些地方中有用到,解决方法就是要么就是改指向的用户模型,要么就是在App\User中也实现JWTSubject接口。我用的是方法二,最后得出的结果:

file

4个月前 评论

Class ‘Tymon\JWTAuth\Providers\LumenServiceProvider’ not found

"laravel/lumen-framework": "^7.0",
"tymon/jwt-auth": "^1.0"
3个月前 评论

谢谢,解决了我的大问题,是不是7.0版本也支持

2个月前 评论

ErrorException: Non-static method Tymon\JWTAuth\JWT::invalidate() should not be called statically in file 出现这个是什么原因?Google 都没有找到答案。laravel6. 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth', 'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory', 这两个已经引入了。是不是providers要引入什么?

2个月前 评论

JWTAuth::parseToken()->invalidate(); 这个是怎么做到让token失效的??

2个月前 评论

为何我不同的guard,用同一个token居然也是可以的 filefilefile

2个月前 评论
zpers 2个月前
zpers 2个月前
Ivey (作者) 1个月前

单点登录如何实现?比如同一个账号密码,不同设备。

2个月前 评论
zpers 2个月前
MrFeelop 1个月前
Johnson16 (作者) 1个月前
mayong 3周前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!