Node.js中JWT的token完整生命周期管理:从生成到销毁

Node.js中JWT的token完整生命周期管理:从生成到销毁

在Node.js中使用JWT(JSON Web Token)进行身份验证和授权是一种常见的实践。下面详细介绍JWT从生成到销毁的过程。

JWT生成

安装jsonwebtoken库

要生成JWT,首先需要安装jsonwebtoken库。通过运行以下命令来安装:

bash 复制代码
npm install jsonwebtoken

创建JWT

在Node.js中生成JWT(JSON Web Token)的过程涉及几个关键步骤,下面将详细介绍这些步骤和相关的技术细节。

1. Header(头部)

JWT的头部通常由两部分组成:令牌的类型(typ)和所使用的签名算法(alg)。例如,对于HS256算法,头部可能如下所示:

json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

这个JSON对象需要被Base64Url编码,以形成JWT的第一个部分

2. Payload(负载)

负载部分包含了所谓的"声明"(Claims),这些是关于实体(通常是用户)和其他数据的陈述。声明分为三类:注册的声明、公共的声明和私有的声明。例如:

json 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

这个JSON对象同样需要被Base64Url编码,以形成JWT的第二部分

3. Signature(签名)

签名是使用Header中指定的签名算法和密钥(Secret)对Header和Payload的编码字符串进行签名的结果。这个过程确保了JWT的完整性和真实性。签名的生成公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

这个签名是JWT的第三部分,并且是验证JWT有效性的关键。

4. 组合Header、Payload和Signature

最终,将编码后的Header、Payload和生成的Signature用点(.)连接起来,形成完整的JWT字符串

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
5. 使用jsonwebtoken库生成JWT

在Node.js中,可以使用jsonwebtoken库来简化JWT的生成过程。以下是一个基本示例:

javascript 复制代码
const jwt = require('jsonwebtoken');
const token = jwt.sign(
    {
        id: 1,
        username: 'admin'
    },
    'secret', // 自定义密钥
    {
        expiresIn: '1h' // 过期时间
    }
);
console.log(token);

在这个示例中,我们使用jwt.sign方法,传入负载(包含用户信息)、密钥和签名选项(如过期时间)来生成JWT。

通过这些步骤,你可以在Node.js应用中生成JWT,用于身份验证和信息交换。生成的JWT可以被客户端存储和使用,直到它过期或被服务器端撤销。

JWT验证

在Node.js中验证JWT的过程涉及几个关键步骤,以下是详细的验证方法:

1. 验证JWT

服务器收到JWT后,需要验证其真实性。使用以下代码验证JWT:

javascript 复制代码
const jwt = require('jsonwebtoken');
const token = 'your-jwt-token';
const secretKey = 'your-secret-key';

try {
  const decoded = jwt.verify(token, secretKey);
  console.log(decoded);
} catch (error) {
  console.error('JWT verification failed');
}

在这个过程中,jwt.verify方法会使用提供的secretKey来验证JWT的签名。如果签名验证成功,它会返回JWT的Payload部分,否则会抛出错误。

2. 创建JWT验证中间件

在Express.js中,你可以创建JWT验证中间件来保护特定路由。以下是一个示例:

javascript 复制代码
const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key';

function authenticateToken(req, res, next) {
  const token = req.header('Authorization');
  if (!token) return res.status(401).send('Access denied');
  try {
    const decoded = jwt.verify(token, secretKey);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).send('Invalid token');
  }
}

这个中间件函数authenticateToken会检查HTTP请求头中的Authorization字段是否包含JWT。如果JWT存在,它会尝试验证JWT的有效性。如果验证成功,decoded对象(包含JWT的Payload)会被附加到req.user上,以便在后续的请求处理中使用。如果验证失败,会返回403错误。

3. 应用中间件保护路由

你可以将authenticateToken中间件应用到需要保护的路由上,以确保只有验证通过的用户才能访问:

javascript 复制代码
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'This is a protected route', user: req.user });
});

在这个例子中,任何尝试访问/protected路由的请求都必须通过authenticateToken中间件的验证。

4. 安全提示

  • 安全存储密钥:密钥是JWT的关键部分,应安全存储。不要硬编码密钥,最好从环境变量或配置文件中读取。
  • 定期刷新令牌:为JWT设置适当的到期时间,以降低安全风险。客户端应定期获取新的JWT令牌。
  • 不要在JWT中存储敏感信息:避免在JWT中存储敏感信息,因为JWT可以被解码。如果需要存储敏感信息,应使用加密而不是签名。

通过这些步骤,你可以在Node.js应用中实现JWT的验证,确保只有持有有效JWT的用户才能访问受限资源。

JWT销毁

客户端销毁

客户端通常负责销毁存储在本地的JWT,例如在浏览器中清除localStorage或sessionStorage中的token,但是这种方法通常不保险,还需要在后端彻底将推出的jwt销毁。这可以通过设置cookie过期时间来实现:

javascript 复制代码
res.cookie('token', 'none', {
  expires: new Date(Date.now()),
})

服务端销毁

由于只在服务端删除浏览器中的localStorage不保险,所以子在服务端也要进行类似的操作,将已经生成的token删除掉。结合本人已经做过的项目代码进行一步步讲解:

1.创建一个tokenmanage表

这个表用来存储每一个登录的账号的登录时间(请求登录接口时候的登录时间)、过期时间(你token设置的过期时间)、用户账号(生成这个token对应的账号,对应用户表的account)

bash 复制代码
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `tokenmanage`
-- ----------------------------
DROP TABLE IF EXISTS `tokenmanage`;
CREATE TABLE `tokenmanage` (
  `id` int(8) NOT NULL AUTO_INCREMENT,
  `token` varchar(255) DEFAULT NULL,
  `logintime` varchar(255) DEFAULT NULL COMMENT '登录时间',
  `expiretime` varchar(255) DEFAULT NULL COMMENT '过期时间',
  `useraccount` varchar(255) DEFAULT NULL COMMENT '用户账号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf32;
2. user表加字段
bash 复制代码
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `users`
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(8) NOT NULL AUTO_INCREMENT,
  `account` varchar(30) DEFAULT NULL,
  `pwd` varchar(30) DEFAULT NULL,
  `utype` int(1) DEFAULT NULL COMMENT '类型',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1 正常  0删除',
  `ctime` varchar(30) DEFAULT NULL COMMENT '创建时间,时间戳',
  `pfcollege` varchar(30) DEFAULT NULL,
  `pfgrade` varchar(10) DEFAULT NULL,
  `pfspeciality` varchar(10) DEFAULT NULL,
  `pfpart` tinyint(1) DEFAULT NULL,
  `uname` varchar(10) DEFAULT NULL,
  `picture` varchar(100) DEFAULT NULL,
  `logintime` varchar(30) DEFAULT NULL COMMENT '登录时间', 
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf32;
2.登录接口处理

所需模块:

javascript 复制代码
var express = require('express')
var router = express.Router()
var mysql = require('mysql')
// 生成token
const webjwt = require('jsonwebtoken')

//  从db文件中导入的数据库模块
var mysql = require("../db/mysql")

登录接口代码:

javascript 复制代码
// 登录接口
router.get('/login', function (req, response) {
    var body = req.query;
    console.log(req.query);

    let { mobile, pwd } = body;
    console.log(mobile, pwd);
    // 先查询账号是否存在
    mysql.query('SELECT account, id, logintime FROM users WHERE account = ?', [mobile], function (error, results) {
        if (error) {
            console.log(error);
            response.json({ code: 1, msg: '数据库查询错误' });
            return;
        }
        if (results.length > 0) {
            // 如果账号存在的话继续查询账号密码
            mysql.query('SELECT account, id FROM users WHERE account = ? AND pwd = ?', [mobile, pwd], function (error, results) {
                if (error) {
                    console.log(error);
                    response.json({ code: 1, msg: '数据库查询错误' });
                    return;
                }
                if (results.length > 0) {
                    let user = results[0];
                    // 更新登录时间
                    let now = new Date().getTime(); // 获取当前时间的时间戳,以毫秒为单位
                    mysql.query('UPDATE users SET logintime = ? WHERE id = ?', [now, user.id], function (error, updateResults) {
                        if (error) {
                            console.log(error);
                            response.json({ code: 1, msg: '数据库更新错误' });
                            return;
                        }
                        // 注意默认情况 Token 必须以 Bearer+空格 开头
                        const token = 'Bearer ' + webjwt.sign(
                            {
                                uid: user.id,
                                account: user.account
                            },
                            'pwdxpx',
                            {
                                expiresIn: 3600 * 24 * 2
                                // expiresIn: 1 * 60
                            }
                        );
                        let threeDaysLater = now + 2 * 24 * 60 * 60 * 1000; // 计算三天后的时间戳
                        // let threeDaysLater = now + 1 * 60 * 1000;

                        // 检查tokenManage表中是否存在对应的useraccount记录,看看这个用户是否登录过
                        mysql.query('SELECT token,logintime,expiretime FROM tokenmanage WHERE useraccount = ?',
                            [user.account],
                            function (error, tokenResults) {
                                console.log(tokenResults[0].logintime, typeof (tokenResults[0].expiretime))
                                if (error) {
                                    console.log(error);
                                    response.json({ code: 1, msg: '数据库查询错误' });
                                    return;
                                }
                                if (tokenResults.length > 0) {
                                    // 如果当前账号存在token,那就判断登录时间是否超过了过期时间,
                                    // 如果登录token已经过期了,那就删除当前记录
                                    if (Number(tokenResults[0].logintime) > Number(tokenResults[0].expiretime)) {
                                        // 删除+插入添加
                                        /* mysql.query("DELETE FROM tokenmanage WHERE useraccount = ?", [user.account], (error, resultsexpire) => {
                                            if (error) console.log(error)
                                            console.log("token 验证已经过期了,接下来重新添加")
                                        })
                                        mysql.query('INSERT INTO tokenmanage (token, logintime, expiretime, useraccount) VALUES (?, ?, ?, ?)',
                                            [token, now, threeDaysLater, user.account],
                                            function (error, insertTokenResults) {
                                                if (error) {
                                                    console.log(error);
                                                } else {
                                                    console.log("token表添加成功!");
                                                }
                                        });*/
                                        // 直接更新 ~~删除+插入添加
                                        mysql.query("UPDATE tokenmanage SET token = ?, logintime = ?, expiretime = ?, useraccount = ?",
                                            [token, now, threeDaysLater, user.account],
                                            (error, updatetokenResults) => {
                                                if (error) {
                                                    console.log(error);
                                                } else {
                                                    console.log("token表更新成功!");
                                                    response.json({
                                                        code: 0, msg: "验证更新-登录完成",
                                                        data: {
                                                            userid: user.id,
                                                            user_account: mobile,
                                                            status: 'ok',
                                                            token: token
                                                        }
                                                    });
                                                }
                                            }
                                        )
                                    } else {
                                        // 如果登录token没有过期了,那就更新当前记录的登录时间改为最新的时间
                                        mysql.query('UPDATE tokenmanage SET logintime = ? WHERE useraccount = ?',
                                            [now, user.account],
                                            function (error, updateTokenResults) {
                                                if (error) {
                                                    console.log(error);
                                                } else {
                                                    console.log("token表的登录时间更新成功!");
                                                }
                                            });
                                        response.json({
                                            code: 0, msg: "登录完成",
                                            data: {
                                                userid: user.id,
                                                user_account: mobile,
                                                status: 'ok',
                                                token: tokenResults[0].token
                                            }
                                        });
                                    }
                                } else {
                                    // 如果当前账号不存在token,那就插入一条新记录
                                    mysql.query('INSERT INTO tokenManage (token, logintime, expiretime, useraccount) VALUES (?, ?, ?, ?)',
                                        [token, now, threeDaysLater, user.account],
                                        function (error, insertTokenResults) {
                                            if (error) {
                                                console.log(error);
                                            } else {
                                                console.log("token表添加成功!");
                                            }
                                        });
                                    // 账号密码一致则返回用户需要的数据
                                    response.json({
                                        code: 0, msg: "登录完成",
                                        data: {
                                            userid: user.id,
                                            user_account: mobile,
                                            status: 'ok',
                                            token: token
                                        }
                                    });
                                }
                            });
                    });
                } else {
                    response.json({
                        code: 1,
                        msg: '用户名或密码不正确',
                    });
                }
            });
        } else {
            // 账号不存在则提示:账号不存在,请输入正确的账号
            response.send({ code: 1, msg: '账号不存在,请输入正确的账号' });
        }
    });
});
3. 退出接口处理
javascript 复制代码
// 退出接口
router.get('/logout', function (req, res, next) {
    // 从请求头中获取Authorization字段,并提取token
    const authHeader = req.headers.authorization;
    const token = authHeader ? authHeader.split(' ')[1] : null; // 提取token
    if (!token) {
        return res.status(401).json({ msg: "未提供有效的token", code: 401 });
    }
    console.log(token);
    // 先通过请求头中提供的token,去数据表中根据token字段查找是否存在该记录
    let sqltoken = `SELECT useraccount, logintime, expiretime FROM tokenmanage WHERE token = ?`;
    mysql.query(sqltoken, [token], (error, results) => {
        if (error) {
            console.log(error);
            return res.status(500).json({ msg: "服务器错误", code: 500 });
        } else {
            // 如果查找语句查出结果代表此token已经存在记录,要做到退出就将此token对应的记录删除即可
            if (results.length === 0) {
                return res.status(404).json({ msg: "未找到token", code: 404 });
            }
            // 假设你只想返回一个退出成功的消息,而不是整个results对象
            var msg = { "msg": "退出成功", code: 0 };
            // 删除tokenmanage表中对应的记录
            let deleteSql = `DELETE FROM tokenmanage WHERE token = ?`;
            mysql.query(deleteSql, [token], (deleteError, deleteResults) => {
                if (deleteError) {
                    console.log(deleteError);
                    return res.status(500).json({ msg: "删除token失败", code: 500 });
                }
                // 如果删除成功,返回成功消息
                res.json(msg);
            });
        }
    });
});

通过上述步骤,你可以在Node.js应用中实现JWT的生成、验证和销毁,从而确保应用的安全性和用户身份的验证。

相关推荐
皓木.37 分钟前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端
CodeChampion1 小时前
60.基于SSM的个人网站的设计与实现(项目 + 论文)
java·vue.js·mysql·spring·elementui·node.js·mybatis
i7i8i9com1 小时前
java 1.8+springboot文件上传+vue3+ts+antdv
java·spring boot·后端
秋意钟1 小时前
Spring框架处理时间类型格式
java·后端·spring
Domain-zhuo1 小时前
如何利用webpack来优化前端性能?
前端·webpack·前端框架·node.js·ecmascript
理想不理想v1 小时前
webpack如何自定义插件?示例
前端·webpack·node.js
我叫啥都行1 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
Stark、1 小时前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端
coding侠客2 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘
java·spring boot·后端