您的位置:首页 > 健康 > 养生 > JWT双令牌认证实现无感Token自动续约

JWT双令牌认证实现无感Token自动续约

2024/10/13 13:08:32 来源:https://blog.csdn.net/weixin_53391173/article/details/141532609  浏览:    关键词:JWT双令牌认证实现无感Token自动续约

概念

JSON Web Token (JWT)是一个开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

虽然可以对 JWT 进行加密,以便在各方之间提供保密性,但是我们将关注已签名的Token。签名Token可以验证其中包含的声明的完整性,而加密Token可以向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,该签名还证明只有持有私钥的一方才是对其进行签名的一方( 签名技术是保证传输的信息不可抵赖,并不能保证信息传输的安全 )

官网地址:https://jwt.io

JWT 原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{"姓名": "八戒","角色": "管理员","到期时间": "2028年12月11日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 数据结构

编码后的数据结构

 

 

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下

Header(头部)
Payload(负载)
Signature(签名)

写成一行,就是下面的样子。

Header.Payload.Signature

JWT 认证流程

认证流程流程说明:

  1. 浏览器发起请求登陆,用户携带用户名和密码等了

  2. 服务端验证身份,根据算法,将用户标识符打包生成 Token,

  3. 服务器返回JWT信息给浏览器,JWT不包含敏感信息

  4. 浏览器发起请求获取用户资料,把刚刚拿到的 Token一起发送给服务器

  5. 服务器发现数据中有 Token,验证身份是否合法

  6. 服务器根据当前Token解析返回该用户的用户资料

双令牌解决方案

在前后端分离的开发模式下,前端用户登录成功后后端服务会给用户颁发一个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_tokenrefresh_token很巧妙的实效设置,达到双令牌刷新、续期。

AccessToken和RefreshToken

什么是 Access Token ?

Access Token 用于基于 Token 的认证模式,允许应用访问一个资源 API。用户认证授权成功后,服务端会签发 Access Token 给应用。应用需要携带 Access Token 访问资源 API,资源服务 API 会通过拦截器查验 Access Token 中的 scope 字段是否包含特定的权限项目,从而决定是否返回资源。

什么是 Refresh Token ?

通常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

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_exp2小时过期

  • refresh_token设置refresh_exp7天过期

生成令牌

$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_typestringToken 类型Bearer
expires_inint凭证有效时间,单位:秒36000
access_tokenstring访问凭证XXXXXXXXXXXXXXXXXXXX
refresh_tokenstring刷新凭证(访问凭证过期使用 )XXXXXXXXXXXXXXXXXXXX

中间件拦截器

/*** @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_token2小时过期后,服务端会返回一个401的HTTP状态码HTTP/1.1 401 Unauthorized,参考如下所示:

HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8{"code": 0,"msg": "身份验证会话已过期,请重新登录!","data": {}
}

现在access_token2小时已过期了,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_tokenrefresh_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;}}
);

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com