JWT 完整使用详解

假期综合征, 需要找一个东西复习复习找回一下PHPer的感觉

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

原文地址: JWT 完整使用详解

安装及基础配置

Laravel

使用 composer 安装

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

进行一些配置

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

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

发布配置文件
1
2
# 这条命令会在 config 下增加一个 jwt.php 的配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
生成加密密钥
1
2
# 这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar
php artisan jwt:secret
更新你的模型

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

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
<?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 [];
}
}
注册两个 Facade

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

  • config/app.php
1
2
3
4
5
6
'aliases' => [
...
// 添加以下两行
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],

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

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

了解更多: Laravel 辅助函数 auth 与 JWT 扩展详解

1
2
3
4
5
// 如果你不用 Facade,你可以这么写
auth('api')->refresh();

// 用 JWTAuth Facade
JWTAuth::parseToken()->refresh();

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

修改 auth.php
  • config/auth.php
1
2
3
4
5
6
7
8
9
10
11
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],

'api' => [
'driver' => 'jwt', // 原来是 token 改成jwt
'provider' => 'users',
],
],
注册一些路由

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

1
2
3
4
5
6
7
8
9
10
11
12
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');

});
创建 token 控制器
1
php artisan make:controller AuthController
  • AuthController

值得注意的是 Laravel 这要用 auth('api')

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?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

使用 composer 安装

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

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

执行

1
composer update

进行一些配置

开启 Facade 和 Eloquent

取消以下行的注释。

  • bootstrap/app.php
1
2
3
// $app->withFacades();

// $app->withEloquent();
开启中间件认证

取消以下行的注释。

  • bootstrap/app.php
1
2
3
4
5
// $app->routeMiddleware([
// 'auth' => App\Http\Middleware\Authenticate::class,
// ]);

// $app->register(App\Providers\AuthServiceProvider::class);
添加服务提供者
  • bootstrap/app.php
1
2
// 有些文档里是说添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,那是旧版本的
$app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);
生成加密密钥

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

1
php artisan jwt:secret
更新你的模型

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

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
36
37
<?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 [];
}
}
注册两个 Facade

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

  • bootstrap/app.php

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

1
2
3
4
$app->withFacades(true, [
'Tymon\JWTAuth\Facades\JWTAuth' => 'JWTAuth',
'Tymon\JWTAuth\Facades\JWTFactory' => 'JWTFactory',
]);
设置 auth.php

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

  • auth.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

...

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => \App\User::class
]
注册一些路由
1
2
3
4
5
6
7
8
9
10
11
12
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');

});
创建 token 控制器

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

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

  • AuthController.php
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?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
]);
}
}

JWT Token 详解

token 的组成、创建以及解析

组成

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

头部(Header)

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

例如:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

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

载荷(Payload)

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

载荷的属性也分三类:

  • 预定义 (Registered)
  • 公有 (Public)
  • 私有 (Private)
预定义的载荷
1
2
3
4
5
6
7
8
9
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1651888119,
"exp": 1654516119,
"nbf": 1651888119,
"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

签名(Signature)

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

1
2
3
4
5
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 代码示例
1
2
3
4
5
6
// 这里要开启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), '+/', '-_'), '=');
}

创建

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

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

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

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

1
2
3
4
5
6
7
// 使用辅助函数
$credentials = request(['email', 'password']);
$token = auth()->attempt($credentials)

// 使用 Facade
$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials);
基于 users 模型返回的实例
1
2
3
4
5
6
7
// 使用辅助函数
$user = User::first();
$token = auth()->login($user);

// 使用 Facade
$user = User::first();
$token = JWTAuth::fromUser($credentials);
基于 users 模型中的主键 id
1
2
3
4
5
// 使用辅助函数
$token = auth()->tokenById(1);

// 使用 Facade
源码中没找到

解析

解析 token 到对象

只有 Facade 需要这样。

1
2
// 把请求发送过来的直接解析到对象
JWTAuth::parseToken();
获取 token 中的 user 信息
1
2
3
4
5
// 辅助函数
$user = auth()->user();

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

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

1
2
3
4
5
// 辅助函数
$token = auth()->getToken();

// Facade
$token = JWTAuth::parseToken()->getToken();
如果是前端

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

载荷的设置和获取

载荷设置

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$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);

载荷解析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 辅助函数
$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');

token 的三个时间

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

有效时间

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

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

刷新时间

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

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

宽限时间

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

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

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

关于 JWT 的讨论

为什么用 JWT?

JWT 超详细分析

token 的刷新问题?

token 为什么要刷新?

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

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

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

附录

JWT 的 两个 Facade

JWTAuth

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

token 生成

attempt

根据 user 账密新建一个 token

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

fromUser or fromSubject

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

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

refresh

更新 token。

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

invalidate

让一个 token 无效。

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

check

检验 token 的有效性。

1
2
3
if(JWTAuth::parseToken()->check()) {
dd("token是有效的");
}
token 解析

authenticate or toUser or user

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

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

parseToken

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

getToken

request 中获取 token

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

JWTGuard

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

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

参考

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2023 Keep It Simple And Stupid All Rights Reserved.

访客数 : | 访问量 :