假期综合征, 需要找一个东西复习复习找回一下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 ; public function getJWTIdentifier ( ) { return $this ->getKey (); } public function getJWTCustomClaims ( ) { return []; } }
注册两个 Facade 这两个 Facade 并不是必须的,但是使用它们会给你的代码编写带来一点便利。
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
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
值得注意的是 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 { public function __construct ( ) { $this ->middleware ('auth:api' , ['except' => ['login' ]]); } public function login ( ) { $credentials = request (['email' , 'password' ]); if (! $token = auth ('api' )->attempt ($credentials )) { return response ()->json (['error' => 'Unauthorized' ], 401 ); } return $this ->respondWithToken ($token ); } public function me ( ) { return response ()->json (auth ('api' )->user ()); } public function logout ( ) { auth ('api' )->logout (); return response ()->json (['message' => 'Successfully logged out' ]); } public function refresh ( ) { return $this ->respondWithToken (auth ('api' )->refresh ()); } 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"
执行
进行一些配置 开启 Facade 和 Eloquent 取消以下行的注释。
1 2 3 // $app->withFacades(); // $app->withEloquent();
开启中间件认证 取消以下行的注释。
1 2 3 4 5 // $app->routeMiddleware([ // 'auth' => App\Http\Middleware\Authenticate::class, // ]); // $app->register(App\Providers\AuthServiceProvider::class);
添加服务提供者
1 2 // 有些文档里是说添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,那是旧版本的 $app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);
生成加密密钥 这条命令会在 .env
文件下生成一个加密密钥,如:JWT_SECRET=foobar
更新你的模型 如果你使用默认的 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 ; ... public function getJWTIdentifier ( ) { return $this ->getKey (); } public function getJWTCustomClaims ( ) { return []; } }
注册两个 Facade Lumen 中没有辅助函数 auth ()
,这两个 Facade 就挺有用了。
把原先去了注释的那一行再改一下。
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
文件夹(没有就新建)。
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-helpers 或 albertcht/lumen-helpers 补全(建议用后者,更好安装)
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 { public function __construct ( ) { $this ->middleware ('auth:api' , ['except' => ['login' ]]); } 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 ); } public function me ( ) { return response ()->json (JWTAuth ::parseToken ()->touser ()); } public function logout ( ) { JWTAuth ::parseToken ()->invalidate (); return response ()->json (['message' => 'Successfully logged out' ]); } public function refresh ( ) { return $this ->respondWithToken (JWTAuth ::parseToken ()->refresh ()); } 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
头部通常由两部分组成:令牌的类型(即 JWT
)和正在使用的签名算法(如 HMAC SHA256
或 RSA
.)。
例如:
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
1 2 3 4 5 6 $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 )$credentials = $request ->only ('email' , 'password' );$token = JWTAuth ::attempt ($credentials );
基于 users 模型返回的实例 1 2 3 4 5 6 7 $user = User ::first ();$token = auth ()->login ($user );$user = User ::first ();$token = JWTAuth ::fromUser ($credentials );
基于 users 模型中的主键 id 1 2 3 4 5 $token = auth ()->tokenById (1 );源码中没找到
解析 解析 token 到对象 只有 Facade
需要这样。
1 2 // 把请求发送过来的直接解析到对象 JWTAuth::parseToken();
获取 token 中的 user 信息 1 2 3 4 5 $user = auth ()->user ();$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
是一种最简单的认证方法,但是由于每次请求都带用户名和密码,频繁的传输肯定不安全,所以才有 cookies
和 session
的运用。如果 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
这三个效果是一样的,toUser
是 authenticate
的别名,而 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);
参考