转自https://learnku.com/articles/10885/full-use-of-jwt
转自https://www.jianshu.com/p/9e95a5f8ac4a
JWT 全称 JSON Web Tokens ,是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。它的两大使用场景是:认证和数据交换。
一.如何安装
#composer 安装
composer require tymon/jwt-auth 1.*@rc
#安装成功后进行配置
添加服务提供商
将下面这行添加至 config/app.php
文件 providers
数组中:
app.php
'providers' => [
...
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
这两个 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();
发布配置文件
在你的 shell 中运行如下命令发布 jwt-auth 的配置文件:
shell
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
此命令会在 config
目录下生成一个 jwt.php
配置文件,你可以在此进行自定义配置。
生成密钥
jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret,你只需要在你的 shell
中运行如下命令即可:
shell
$ php artisan jwt:secret
此命令会在你的 .env
文件中新增一行 JWT_SECRET=secret
。
配置 Auth guard
在 config/auth.php
文件中,你需要将 guards/driver
更新为 jwt
:
注意如果模型不为user 则进行更改 providers 中 model
auth.php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
...
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
更改 Model
如果需要使用 jwt-auth 作为用户认证,我们需要对我们的 User
模型进行一点小小的改变,实现一个接口,变更后的 User
模型如下:
User.php
<?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 [];
}
}
配置项详解
jwt.php
<?php
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| 用于加密生成 token 的 secret
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT Authentication Keys
|--------------------------------------------------------------------------
|
| 如果你在 .env 文件中定义了 JWT_SECRET 的随机字符串
| 那么 jwt 将会使用 对称算法 来生成 token
| 如果你没有定有,那么jwt 将会使用如下配置的公钥和私钥来生成 token
|
*/
'keys' => [
/*
|--------------------------------------------------------------------------
| Public Key
|--------------------------------------------------------------------------
|
| 公钥
|
*/
'public' => env('JWT_PUBLIC_KEY'),
/*
|--------------------------------------------------------------------------
| Private Key
|--------------------------------------------------------------------------
|
| 私钥
|
*/
'private' => env('JWT_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| Passphrase
|--------------------------------------------------------------------------
|
| 私钥的密码。 如果没有设置,可以为 null。
|
*/
'passphrase' => env('JWT_PASSPHRASE'),
],
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 有效的时间长度(以分钟为单位),默认为1小时,您也可以将其设置为空,以产生永不过期的标记
|
*/
'ttl' => env('JWT_TTL', 60),
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 可刷新的时间长度(以分钟为单位)。默认的时间为 2 周。
| 大概意思就是如果用户有一个 access_token,那么他可以带着他的 access_token
| 过来领取新的 access_token,直到 2 周的时间后,他便无法继续刷新了,需要重新登录。
|
*/
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| 指定将用于对令牌进行签名的散列算法。
|
*/
'algo' => env('JWT_ALGO', 'HS256'),
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| 指定必须存在于任何令牌中的声明。
|
|
*/
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
/*
|--------------------------------------------------------------------------
| Persistent Claims
|--------------------------------------------------------------------------
|
| 指定在刷新令牌时要保留的声明密钥。
|
*/
'persistent_claims' => [
// 'foo',
// 'bar',
],
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| 为了使令牌无效,您必须启用黑名单。
| 如果您不想或不需要此功能,请将其设置为 false。
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
| -------------------------------------------------------------------------
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| 当多个并发请求使用相同的JWT进行时,
| 由于 access_token 的刷新 ,其中一些可能会失败
| 以秒为单位设置请求时间以防止并发的请求失败。
|
*/
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| 指定整个包中使用的各种提供程序。
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| 指定用于创建和解码令牌的提供程序。
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| 指定用于对用户进行身份验证的提供程序。
|
*/
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| 指定用于在黑名单中存储标记的提供程序。
|
*/
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
],
];
使用
现在,我们可以在我们的 routes/api.php
路由文件中新增几条路由来测试一下了:
api.php
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');
});
在你的 shel
l 中运行如下命令以新增一个控制器:
$ php artisan make:controller AuthController
打开此控制器,写入如下内容
<?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
]);
}
}
可以通过Postman检验结果
踩坑点 login 路由的时候记得要先生成一条记录、
php artisan tinker
namespace App;
Users::create(['name' => 'Test','email' => '17623239881@qq.com','password' => bcrypt('123456')]);
----------------------------
----------------------------
=> App\Users {#2926
name: "Test",
email: "17623239881@qq.com",
updated_at: "2019-05-15 08:44:52",
created_at: "2019-05-15 08:44:52",
id: 21,
}
有两种使用方法:
加到 url 中:
?token=你的token
- 加到 header 中,建议用这种,因为在 https 情况下更安全:
Authorization:Bearer 你的token,是header
注意 模型中有没有重构getAuthPassword方法 重构此方法可能导致 验证登录失败
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 指出,其实并不是这么绝对,具体细节,看我们上面 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
是一种最简单的认证方法,但是由于每次请求都带用户名和密码,频繁的传输肯定不安全,所以才有 cookies
和 session
的运用。如果 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
这三个效果是一样的,toUser
是 authenticate
的别名,而 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);