一、JWT概念
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_token和refresh_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_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..."
}
参数描述
六、中间件拦截器
/**
* @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;
}
}
);
var first_sceen__time = (+new Date());
if ("" == 1 && document.getElementById('js_content')) {
document.getElementById('js_content').addEventListener("selectstart",function(e){ e.preventDefault(); });
}