我们相信:世界是美好的,你是我也是。平行空间的世界里面,不同版本的生活也在继续...

以应用最为广泛的JWT令牌为载体,说明为什么会出现两个令牌,access_tokenrefresh_token两者有什么区别?应该如何使用?令牌存储在什么地方比较合适?

苏南大叔:JWT令牌,access_token和refresh_token的区别和联系 - 双token概述
JWT令牌,access_token和refresh_token的区别和联系(图1-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。测试环境:nodejs@20.18.0,express@4.21.2jsonwebtoken@9.0.2express-jwt@8.5.1

两个令牌

实际的应用中,令牌多为两个:access_tokenrefresh_token

名称有效期作用保密性
access_token较短,1h/1d 等请求保护数据时使用建议在https下使用
refresh_token较长,7d/30d 等请求新的双token更高的保密要求

这里主要考虑的就是token被中间人盗取的情况。不做特殊处理情况下,token是没有办法主动废除的。它一旦被发出,在有效期结束之前都视为有效。那么,一旦有中间人拿到了这个token,事实上就可以获得这个账户的全部权限。所以,业内想到的办法是:尽量减少access_token的有效期,被盗后也会很快失效,减少危险。

减少access_token有效期的同时,带来的问题就是:频繁登陆。新的应对方式就是refresh_token,有效期较长,它用于获取新的access_tokenrefresh_token。但是,它也面临着中间人盗取的问题。所以,它的保密性要求更高。比如:

  • 存储在更安全的位置(oauth2.0要求存储在服务器端)。
  • 更少的使用频率(非必要别刷新,refresh_token过期前,在合适的时机刷新一次就行)。

所以,整体的思路就是:化整为零,整体永久有效,局部很快过期。

token数据组成

jwtsecret,保管的并不是token的第二部分主体数据payload,而是token的第三部分verify。所以,主体payload部分的内容,不要放置私密内容,有可能的话,先加密一次再生成token

access_tokenrefresh_token,最大的区别就是:有效期不同。另外,两者千万【不能使用相同的secret】,否则,access_token也可以传递过去变成refresh_token,整个双token体系就面临着信用崩塌了。

苏南大叔建议,还可以在payload内容上做些文章。在能够识别出当前用户的前提下,增加token的类别属性,整体增加保密性。

公共代码

下面的是公共代码代码:

const express = require("express");
const app = express();
app.use(express.json());
const path = require("path");
app.get("/", function (req, res) {
  let root = path.join(process.cwd(), "wwwroot");
  res.sendFile(path.join(root, "form.html"));
});
app.get("/favicon.ico", function (req, res) {
  res.send("");
});
//##################################
// 重点代码放这里
//##################################
let server = app.listen(3222, function () {
  let port = server.address().port;
  console.log(`访问地址为 http://localhost:${port}`);
});

重点代码中的jwt公共部分,代码如下:

//##################################
// jwt的公共部分
//##################################
const jwt = require("jsonwebtoken");
const { expressjwt } = require("express-jwt");
const options_access = {
  secret: "sunan_access",
  algorithms: ["HS256"],
};
const options_access_sign = {
  expiresIn: "3h", //3小时后的任何接口操作,会引发refresh
  algorithm: options_access.algorithms[0],
};
const options_refresh = {
  secret: "sunan_refresh",
  algorithms: ["HS384"],
};
const options_refresh_sign = {
  expiresIn: "30d", //30天都不动一下的话,就默认登出了
  algorithm: options_refresh.algorithms[0],
};
let authCheckAccess = expressjwt(options_access);
let authCheckRefresh = expressjwt(options_refresh);

JWT颁发与验证

这里还是以JsonWebToken为例,说一下两个令牌的颁发与验证问题。先回顾一下前文:

首次颁发

第一次登陆成功,颁发access_tokenrefresh_token

app.post("/login", (req, res) => {
  const { user, password } = req.body;
  if (user == "sunan" && password == "dashu") {
    const access_token = jwt.sign(
      { user, type: "access" },
      options_access.secret,
      options_access_sign
    );
    const refresh_token = jwt.sign(
      { user, type: "refresh" },
      options_refresh.secret,
      options_refresh_sign
    );
    res.json({ access_token, refresh_token });
  } else {
    res.json({ message: "用户名或密码错误" });
  }
});

验证令牌

这里要分情况讨论了,

  • 登陆接口,不验证令牌。
  • 刷新令牌接口,验证refresh_token
  • 普通资源保护接口,验证access_token

下面代码演示,普通可保护资源的情况。

app.get("/api/*", authCheckAccess, (req, res) => {
  // 鉴定失败的话,在中间件环节就各种401了,走不到这里
  if (req.auth && req.auth.type === "access") {
    res.send({
      url: req.url,
      username: req.auth.user,
      type: req.auth.type,
    });
  } else {
    // 正常来说,走不到这里...
    res.status(401).send("错误的token类型");
  }
});

刷新令牌

刷新的时机,只要是fresh_token过期前就行。比较合适的时机是:access_token被爆过期的时候。注意,这里验证的是refresh_token,其配置options【必须有别于】access_tokenoptions

app.get("/refresh", authCheckRefresh, (req, res) => {
  // 鉴定失败的话,在中间件环节就各种401了,走不到这里
  if (req.auth && req.auth.type === "refresh") {
    const access_token = jwt.sign(
      { user: req.auth.user, type: "access" },
      options_access.secret,
      options_access_sign
    );
    const refresh_token = jwt.sign(
      { user: req.auth.user, type: "refresh" },
      options_refresh.secret,
      options_refresh_sign
    );
    res.send({
      url: req.url,
      username: req.auth.user,
      type: req.auth.type,
      access_token,
      refresh_token,
    });
  } else {
    // 正常来说,走不到这里...
    res.status(401).send("错误的token类型");
  }
});

错误处理

JsonWebToken验证令牌失败的话,并不会触发任何错误的。但是,express-jwt这个中间件,会疯狂发出各种401状态码。并且会阻止路由对应函数体内的逻辑执行。所以,这个中间件加的是有些霸道。

如果,想要对这些错误信息进行定制,就需要express的全局错误处理代码了。下面的代码,可以做些判断。

app.use((err, req, res, next) => {
  if (err.name == "UnauthorizedError") {
    console.log(req.url);
    console.log(err.name);     // UnauthorizedError
    console.log(err.code);     // invalid_token
    console.log(err.status);   // 401
    console.log(err.message);  // jwt malformed
    // return res.status(401).send("无效的Token");
  }
  return res.send({
    code: 200,
    message: "未知的错误",
  });
});

由于这里的代码,和前端代码的处理连系比较紧密。本文就暂不做探讨修改。伏笔后文。

结语

本文以express+jwt为例,讨论了服务器端对双token的处理方式。并不涉及前端部分,对于access_tokenrefresh_token的处理。这些内容,另开文章讨论。

另外,本文的双token处理,虽然和oauth2.0有联系,但是并非oauth2.0,因为oauth除了服务器和客户端外,还涉及到了第三方身份的介入处理。

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   token