JSON Web Token (JWT)是一个开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
虽然可以对 JWT 进行加密,以便在各方之间提供保密性,但是我们将关注已签名的Token。签名Token可以验证其中包含的声明的完整性,而加密Token可以向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,该签名还证明只有持有私钥的一方才是对其进行签名的一方( 签名技术是保证传输的信息不可抵赖,并不能保证信息传输的安全 )
官网地址:https://jwt.io
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "开源技术小栈",
"角色": "管理员",
"到期时间": "2028年12月11日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
编码后的数据结构

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下
Header(头部)
Payload(负载)
Signature(签名)
写成一行,就是下面的样子。
Header.Payload.Signature


认证流程流程说明:
在前后端分离的开发模式下,前端用户登录成功后后端服务会给用户颁发一个JWT的access_token。前端在接收到JWT的access_token后会将access_token存储到浏览器LocalStorage中。
后续每次请求都会将此access_token放在请求头中传递到后端服务,后端服务会有一个过滤器对access_token进行拦截校验,校验access_token是否过期,如果access_token过期则会让前端跳转到登录页面重新登录。
因为JWT的access_token中一般会包含用户的基础信息,为了保证JWT的access_token的安全性,一般会将JWT的access_token的过期时间设置的比较短。
但是这样又会导致前端用户需要频繁登录(access_token过期),甚至有的表单比较复杂,前端用户在填写表单时需要思考较长时间,等真正提交表单时后端校验发现access_token过期失效了不得不跳转到登录页面。
如果真发生了这种情况前端用户肯定是要吐槽的,对用户体验非常不友好。例如:access_token有效期是2h,用户一直在使用客户端考试,使用的过程中,access_token到期跳转到登录页面邀请重新登录。心里想说什么垃圾系统,过了2个小时又要重新登录!我他妈想骂人了,一万个....
本篇内容就是在前端用户无感知的情况下实现access_token的自动续期,避免频繁登录、表单填写内容丢失情况的发生。以及access_token和refresh_token很巧妙的实效设置,达到双令牌刷新、续期。
Access Token 用于基于 Token 的认证模式,允许应用访问一个资源 API。用户认证授权成功后,服务端会签发 Access Token 给应用。应用需要携带 Access Token 访问资源 API,资源服务 API 会通过拦截器查验 Access Token 中的 scope 字段是否包含特定的权限项目,从而决定是否返回资源。
通常Access Token有效时间通常较短。通常用户在获取资源的时候需要携带 Access Token,当 Access Token 过期后,用户需要获取一个新的 AccessToken。这时候就需要Refresh Token了。Refresh Token 用于获取新的 AccessToken。这样可以缩短 AccessToken 的过期时间保证安全,同时又不会因为频繁过期重新要求用户登录。
用户在初次认证时,Refresh Token 会和AccessToken 一起返回。应用必须安全地存储 Refresh Token,它的重要性和密码是一样的,因为 Refresh Token 能够一直让用户保持登录。

{
"code": 0,
"msg": "success",
"data": {
"token_type": "Bearer",
"expires_in": 7200,
"access_token": "eyJ0eXA1NiJ9.eyJpc3MiOikifX0._kwtyMsMI0ML0o",
"refresh_token": "eyJ0eXiJIUzI1NiJ9.eyJpc3MiOifX0.mYSXrpoNpU"
}
}
}
客户端应用携带 Refresh Token 向服务端点发起请求时,服务端每次都会返回相同的Refresh Token 和新的 AccessToken,直到 Refresh Token 过期。

{
"code": 0,
"msg": "success",
"data": {
"token_type": "Bearer",
"expires_in": 7200,
"access_token": "eyJ0eXA1NiJ9.eyJpc3MiOikifX0._kwtyMsMI0ML0o",
"refresh_token": "eyJ0eXiJIUzI1NiJ9.eyJpc3MiOifX0.mYSXrpoNpU"
}
}
}
实现配置参数说明。access_token设置为2小时过期,而refresh_token设置7天过期。
这样7天内,如果access_token过期了,那就可以用refresh_token来刷新拿到新的access_token。只要不超过7天内未访问系统,那就可以一直是登录状态,可以无限续签,不需要登录。如果超过7天未访问系统,那么refresh_token也就过期了,这时候需要重新登录了。
composer require tinywan/jwt
插件地址:
https://www.workerman.net/plugin/10
配置文件
config/plugin/tinywan/jwt
<?php
return [
'enable' => true,
'jwt' => [
// 算法类型 HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、Ed25519
'algorithms' => 'HS256',
// access令牌秘钥
'access_secret_key' => '2024d3d3LmJq',
// access令牌过期时间,单位:秒。默认 2 小时
'access_exp' => 7200,
// refresh令牌秘钥
'refresh_secret_key' => '2022KTxigxc9o50c',
// refresh令牌过期时间,单位:秒。默认 7 天
'refresh_exp' => 604800,
// refresh 令牌是否禁用,默认不禁用 false
'refresh_disable' => false,
// 令牌签发者
'iss' => 'webman.tinywan.cn',
...
];
access_token设置access_exp为2小时过期refresh_token设置refresh_exp为7天过期$user = [
'id' => 2024,
'name' => 'Tinywan',
'email' => 'Tinywan@163.com'
];
$token = Tinywan\Jwt\JwtToken::generateToken($user);
var_dump(json_encode($token));
输出(json格式)
{
"token_type": "Bearer",
"expires_in": 36000,
"access_token": "eyJ0eXAiOiJAUR-Gqtnk9LUPO8IDrLK7tjCwQZ7CI...",
"refresh_token": "eyJ0eXAiOiJIEGkKprvcccccQvsTJaOyNy8yweZc..."
}
参数描述
参数 | 类型 | 描述 | 示例值 |
|---|---|---|---|
token_type | string | Token 类型 | Bearer |
expires_in | int | 凭证有效时间,单位:秒 | 36000 |
access_token | string | 访问凭证 | XXXXXXXXXXXXXXXXXXXX |
refresh_token | string | 刷新凭证(访问凭证过期使用 ) | XXXXXXXXXXXXXXXXXXXX |
<?php
/**
* @desc 中间件拦截器
* @author Tinywan(ShaoBo Wan)
*/
declare(strict_types=1);
namespace app\middleware;
use Tinywan\ExceptionHandler\Exception\ForbiddenHttpException;
use Tinywan\ExceptionHandler\Exception\UnauthorizedHttpException;
use Tinywan\Jwt\JwtToken;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class AuthorizationMiddleware implements MiddlewareInterface
{
/**
* @param Request $request
* @param callable $handler
* @return Response
* @throws ForbiddenHttpException|UnauthorizedHttpException
*/
public function process(Request $request, callable $handler): Response
{
$request->userId = JwtToken::getCurrentId();
if (0 === $request->userId) {
throw new UnauthorizedHttpException();
}
return $handler($request);
}
}
中间件拦截器中是对 access_token进行请求拦截校验,判断access_token是否有效。如果当前用户access_token无效,则直接拦截请求并返回UnauthorizedHttpException认证失败异常类响应。
令牌验证 无效 响应参考示例
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8
{
"code": 0,
"msg": "令牌会话已过期,请再次登录!",
"data": {}
}
令牌验证 通过 响应参考示例
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"code": 0,
"msg": "success",
"data": {
"id": 202801,
"username": "Tinywan"
},
}
通过以上可以看出我们设置的access_token为2小时过期后,服务端会返回一个401的HTTP状态码HTTP/1.1 401 Unauthorized,参考如下所示:
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8
{
"code": 0,
"msg": "身份验证会话已过期,请重新登录!",
"data": {}
}
现在access_token是2小时已过期了,2小时之后就需要重新登录了。也就是前端需要跳转到登录页面。这样显然体验不好,接下来实现用refresh_token来刷新获取新的访问令牌access_token
通过调用刷新令牌refreshToken()方法来获取最新的访问令牌access_token
刷新令牌伪代码参考
/**
* @desc: 刷新令牌
* @return Response
* @author Tinywan(ShaoBo Wan)
*/
public function refreshToken(): Response
{
$res = \Tinywan\Jwt\JwtToken::refreshToken();
return response_json(0,'success',$res);
}

CUL 模拟请求
curl --request GET \
--url http://127.0.0.1:8888/oauth/refresh-token \
--header 'Accept: */*' \
--header 'Accept-Encoding: gzip, deflate, br' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3ZWJtYW4udGlueXdhbi5jbiIsImF1ZCI6IndlYm1hbi50aW55d2FuLmNuIiwiaWF0IjoxNzI0MTM3MzQzLCJuYmYiOjE3MjQxMzczNDMsImV4cCI6MTcyNDc0MjE0MywiZXh0ZW5kIjp7ImlkIjoyMDIyMDAwMSwidXNlcm5hbWUiOiJ3ZWJtYW4iLCJtb2JpbGUiOiIxMzY2OTM2MTE5MiIsImVtYWlsIjoiVGlueXdhbkAxNjMuY29tIiwiYXZhdGFyIjoiaHR0cHM6Ly9saXZlLW9zcy5iYWlkdS5jb20vYXNzZXRzL2ltYWdlcy9hdmF0YXJzLzZhdmF0YXIuanBnIiwicGFzc3dvcmQiOiIkMnkkMTAkRm1Ka0RJV2JWN2hDTEl0VWV1amhpT0dibDEuVHYwUjRXNEJnaFhZWWNkcThQTGJVNm5lTGUiLCJpc19lbmFibGVkIjoxLCJjcmVhdGVfdGltZSI6IjIwMjEtMTEtMTIgMTA6NDg6NTkifX0.3Ii4Og8N6M7rk9GDxT_RydX12FdioGJUXvJU4wm5AwA' \
--header 'Connection: keep-alive' \
--header 'User-Agent: PostmanRuntime-ApipostRuntime/1.1.0'
注意:这时候请求认证Header的
Authorization: Bearer传的值是refresh_token令牌,而不是access_token令牌.
通过以上请求带上有效的refresh_token,拿到新的access_token和refresh_token
HTTP/1.1 402 Unauthorized
Content-Type: application/json;charset=UTF-8
{
"code": 0,
"msg": "刷新令牌会话已过期,请重新登录!",
"data": {}
}
注意:这里返回的HTTP状态码是
402,当然了该状态码可以通过配置文件进行配置。
可以看出我们设置的refresh_token超过7天也就过期了,这时候需要前端跳转到登录页面让用户重新登录了。
async function refreshToken() {
const res = await axios.get("http://127.0.0.1:8888/oauth/refresh-token", {
params: { refresh_token: localStorage.getItem("refresh_token") },
});
localStorage.setItem("access_token", res.data.access_token || "");
localStorage.setItem("refresh_token", res.data.refresh_token || "");
return res;
}
axios.interceptors.response.use(
(response) => response,
async (err) => {
let { data, config } = err.response;
if (data.statusCode === 401 && config.url.includes("/oauth/refresh-token")) {
const res = await refreshToken();
if (res.status === 200) {
return axios(config);
} else {
alert("登录过期,请重新登录");
return Promise.reject(res.data);
}
} else {
return err.response;
}
}
);