JWT令牌,access_token和refresh_token的区别和联系
发布于 作者:苏南大叔 来源:程序如此灵动~以应用最为广泛的JWT
令牌为载体,说明为什么会出现两个令牌,access_token
和refresh_token
两者有什么区别?应该如何使用?令牌存储在什么地方比较合适?
苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。测试环境:nodejs@20.18.0
,express@4.21.2
,jsonwebtoken@9.0.2
,express-jwt@8.5.1
。
两个令牌
实际的应用中,令牌多为两个:access_token
和refresh_token
。
名称 | 有效期 | 作用 | 保密性 |
---|---|---|---|
access_token | 较短,1h/1d 等 | 请求保护数据时使用 | 建议在https下使用 |
refresh_token | 较长,7d/30d 等 | 请求新的双token | 更高的保密要求 |
这里主要考虑的就是token
被中间人盗取的情况。不做特殊处理情况下,token
是没有办法主动废除的。它一旦被发出,在有效期结束之前都视为有效。那么,一旦有中间人拿到了这个token
,事实上就可以获得这个账户的全部权限。所以,业内想到的办法是:尽量减少access_token
的有效期,被盗后也会很快失效,减少危险。
减少access_token
有效期的同时,带来的问题就是:频繁登陆。新的应对方式就是refresh_token
,有效期较长,它用于获取新的access_token
和refresh_token
。但是,它也面临着中间人盗取的问题。所以,它的保密性要求更高。比如:
- 存储在更安全的位置(
oauth2.0
要求存储在服务器端)。 - 更少的使用频率(非必要别刷新,
refresh_token
过期前,在合适的时机刷新一次就行)。
所以,整体的思路就是:化整为零,整体永久有效,局部很快过期。
token数据组成
jwt
的secret
,保管的并不是token
的第二部分主体数据payload
,而是token
的第三部分verify
。所以,主体payload
部分的内容,不要放置私密内容,有可能的话,先加密一次再生成token
。
access_token
和refresh_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_token
和refresh_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_token
的options
。
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_token
和refresh_token
的处理。这些内容,另开文章讨论。
另外,本文的双token
处理,虽然和oauth2.0
有联系,但是并非oauth2.0
,因为oauth
除了服务器和客户端外,还涉及到了第三方身份的介入处理。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。