NodeJs 第二十三章(Token+登录接口)

343 阅读6分钟

JWT

JWT 即 JSON Web Token,是一种用于在网络应用间安全传递声明的开放标准(RFC 7519)。它通常由三部分组成,使用 JSON 对象作为声明载体,并通过数字签名确保其安全性,在身份验证和信息交换场景中被广泛应用。以下为你详细介绍:

结构

JWT 通常由三部分组成,各部分之间用点(.)分隔,其格式为 header.payload.signature

  • Header(头部) :包含两部分信息,令牌的类型(通常是 JWT)和使用的签名算法,如 HMAC SHA256 或 RSA。它会被 Base64Url 编码形成 JWT 的第一部分。
{
    "alg": "HS256",
    "typ": "JWT"
}
  • Payload(负载) :包含声明(Claims),声明是关于实体(通常是用户)和其他数据的声明。声明分为三种类型:

    • 注册声明:如 iss(发行人)、sub(主题)、aud(受众)等,这些是预定义的声明,但使用并非强制。

    • 公开声明:由各方自由定义。

    • 私有声明:在同意使用的各方之间定义的声明。

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
}
  • Signature(签名) :为了创建签名部分,需要使用编码后的 Header、编码后的 Payload、一个密钥(secret)和 Header 中指定的签名算法。例如,使用 HMAC SHA256 算法时,签名将按以下方式创建:
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

签名用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证 JWT 的发送者的身份。

工作原理

image.png

  1. 用户登录:用户向服务器发送包含用户名和密码的登录请求。
  2. 服务器验证:服务器验证用户的凭证,如果验证通过,服务器会根据预定义的规则生成一个 JWT。
  3. 返回 JWT:服务器将生成的 JWT 返回给客户端。
  4. 客户端存储和使用:客户端收到 JWT 后,通常将其存储在本地(如 localStorage 或 cookie)。在后续的请求中,客户端会在请求头中添加 Authorization 字段,其值为 Bearer <JWT>,将 JWT 发送给服务器。
  5. 服务器验证 JWT:服务器接收到请求后,会从请求头中提取 JWT,并使用之前生成 JWT 时的密钥对其进行验证。如果验证通过,服务器会认为请求是合法的,并返回相应的响应。

优点

  • 无状态:JWT 是无状态的,服务器不需要在会话中存储用户的状态信息,这使得服务器更容易扩展,并且可以方便地处理跨域请求。
  • 可扩展性:由于 JWT 可以包含自定义的声明,因此可以根据需要添加额外的信息,如用户角色、权限等。
  • 跨域支持:JWT 可以通过 HTTP 请求头或 URL 参数进行传输,因此可以很方便地在不同的域名之间传递,适用于前后端分离的架构和微服务架构。

缺点

  • 安全性风险:如果 JWT 的密钥泄露,攻击者可以伪造合法的 JWT,从而绕过身份验证。此外,由于 JWT 通常会在客户端存储,存在被 XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)攻击的风险。
  • 令牌体积:JWT 通常包含较多的信息,因此其体积相对较大,可能会增加网络传输的负担。
  • 难以撤销:一旦 JWT 被签发,在其过期之前很难撤销,除非在服务器端维护一个黑名单。

应用场景

  • 身份验证:在用户登录后,服务器生成一个 JWT 并返回给客户端,客户端在后续的请求中携带该 JWT,服务器通过验证 JWT 来确认用户的身份。
  • 信息交换:JWT 可以安全地在不同的服务之间传递信息,例如在微服务架构中,各个服务可以通过验证 JWT 来获取用户的相关信息。

Express 使用JWT

安装依赖

1. 安装依赖

首先,需要安装 jsonwebtoken 库,它可以帮助我们生成和验证 JWT。在项目根目录下使用以下命令进行安装:

npm install jsonwebtoken
1. 生成 JWT

运用 jwt.sign() 方法能够生成 JWT,此方法接收三个主要参数:

  • payload:要包含在 JWT 里的信息,通常是用户 ID、角色之类的数据。

  • secretOrPrivateKey:用于签名的密钥,它能保证 JWT 的完整性和安全性。

  • options:可选参数,可设置 JWT 的有效期、算法等。

const jwt = require('jsonwebtoken');

// 定义密钥
const secretKey = 'yourSecretKey';

// 定义 payload
const payload = {
    userId: 1,
    username: 'user123'
};

// 生成 JWT
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
console.log('Generated Token:', token);
2. 验证 JWT

使用 jwt.verify() 方法可以验证 JWT 的有效性,该方法接收三个参数:

  • token:要验证的 JWT。
  • secretOrPublicKey:用于验证签名的密钥,必须和生成时用的密钥一致。
  • callback:验证完成后的回调函数,包含错误信息和解析后的 payload。
const jwt = require('jsonwebtoken');
const secretKey = 'yourSecretKey';
const token = 'yourGeneratedToken';

jwt.verify(token, secretKey, (err, decoded) => {
    if (err) {
        console.error('Verification failed:', err.message);
    } else {
        console.log('Verified Payload:', decoded);
    }
});
3. 解码 JWT

jwt.decode() 方法能在不验证签名的情况下对 JWT 进行解码,获取其 payload 信息。不过要注意,这种方式不会验证 JWT 的有效性,所以不能用来做身份验证。

const jwt = require('jsonwebtoken');
const token = 'yourGeneratedToken';

const decoded = jwt.decode(token);
console.log('Decoded Payload:', decoded);

2. 生成 JWT

在用户登录成功后,服务器需要生成一个 JWT 并返回给客户端。以下是一个简单的 Express 示例:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your_secret_key'; // 用于签名 JWT 的密钥,应妥善保管

// 模拟用户登录接口
app.post('/login', (req, res) => {
    // 这里简单假设用户验证通过
    const user = { id: 1, username: 'john_doe' };
    // 生成 JWT
    const token = jwt.sign(user, secretKey, { expiresIn: '1h' });
    res.json({ token });
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在上述代码中,jwt.sign() 方法用于生成 JWT。它接受三个参数:

  • 第一个参数是要包含在 JWT 中的数据(通常是用户信息)。
  • 第二个参数是用于签名的密钥。
  • 第三个参数是可选的配置对象,可以设置 JWT 的过期时间等信息。

3. 验证 JWT

客户端在后续的请求中需要携带 JWT,服务器则需要验证这个 JWT 的有效性。以下是一个验证 JWT 的中间件示例:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your_secret_key';

// 验证 JWT 的中间件
const verifyToken = (req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(403).send('没有提供令牌');
    }
    const tokenWithoutBearer = token.replace('Bearer ', '');
    jwt.verify(tokenWithoutBearer, secretKey, (err, decoded) => {
        if (err) {
            return res.status(401).send('无效的令牌');
        }
        req.user = decoded; // 将解码后的用户信息挂载到请求对象上
        next();
    });
};

// 模拟受保护的接口
app.get('/protected', verifyToken, (req, res) => {
    res.json({ message: '这是受保护的接口', user: req.user });
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在上述代码中,verifyToken 是一个中间件函数,用于验证请求头中携带的 JWT。如果验证通过,将解码后的用户信息挂载到 req.user 上,并调用 next() 继续处理请求;如果验证失败,返回相应的错误信息。

4. 完整示例

将登录和受保护接口整合到一个完整的 Express 应用中:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your_secret_key';

// 验证 JWT 的中间件
const verifyToken = (req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(403).send('没有提供令牌');
    }
    const tokenWithoutBearer = token.replace('Bearer ', '');
    jwt.verify(tokenWithoutBearer, secretKey, (err, decoded) => {
        if (err) {
            return res.status(401).send('无效的令牌');
        }
        req.user = decoded;
        next();
    });
};

// 模拟用户登录接口
app.post('/login', (req, res) => {
    const user = { id: 1, username: 'john_doe' };
    const token = jwt.sign(user, secretKey, { expiresIn: '1h' });
    res.json({ token });
});

// 模拟受保护的接口
app.get('/protected', verifyToken, (req, res) => {
    res.json({ message: '这是受保护的接口', user: req.user });
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

通过以上步骤,你可以在 Express 应用中实现基本的 JWT 身份验证和授权功能。

自己写一遍完整的+测试

const express = require('express');
const app = express();
const port = 3000;
const yaml = require('js-yaml')
const mysql = require("mysql2/promise");
const fs = require('fs')
const bodyParser = require('body-parser');
const cors = require('cors');
const jwt = require('jsonwebtoken');



// 定义密钥
const secretKey = 'yourSecretKey';
// 生成 JWT

(async()=>{
    const db = yaml.load(fs.readFileSync('./db.yaml', 'utf8'))
    // 使用 body-parser 中间件解析 JSON 请求体
    app.use(bodyParser.json());
    // // 解决跨域
    app.use(cors());

    // 创建连接
    const  connection =  await mysql.createConnection({
        host:db.host,  // 主机地址
        port:db.port,  // 端口  默认为 3306
        database:db.database,  // 数据库名称
        user:db.user,  // 用户名
        password:db.password  // 密码
    });
    
    // 注册账户接口
    app.post('/register', async (req, res) => {
        const { username, password } = req.body;
        // 验证请求体
        if (!username || !password) {
             res.status(400)
             return res.json({
                message: '缺少参数'
             });
        }
        try {
            // 检查用户名是否已存在
            const [rows, fields]  = await connection.execute('SELECT * FROM users WHERE username = ?', [username]);
            if (rows.length > 0) {
                res.status(409)
               return res.json({
                    message: '用户名已存在'
               });
            }
    
            // // 插入新用户
            await connection.execute('INSERT INTO users (username, password ) VALUES (?, ?)', [username, password]);
              res.status(201)
              return res.json({ message: '注册成功' });
    
        } catch (error) {
            console.error(error);
            res.status(500).json({ message: '服务器内部错误' });
        }
    });


    app.post('/login', async (req, res) => {
        const { username, password } = req.body;
        // 验证请求体
        if (!username ||!password) {
             res.status(400)
             return res.json({
                message: '缺少参数'
             });
        }
        try {
            // 检查用户名是否已存在
            const [rows, fields]  = await connection.execute('SELECT * FROM users WHERE username =?', [username]);
            if (rows.length === 0) {
                res.status(404)
               return res.json({
                    message: '用户名不存在'
               });
            }
            if(rows[0].password !== password){
                res.status(401)
                return res.json({
                    message: '密码错误'
                })
            }

            console.log(jwt);
            
            console.log(rows[0].username);
            
            const token = jwt.sign({data:rows[0].username}, secretKey, { expiresIn: '1h' });
            // 将 JWT 发送给客户端
            res.status(200)
            return res.json({
                message: '登录成功',
                status:200, 
                token
            })
        }
        catch (error) {
            console.error(error);
            res.status(500).json({ message: '服务器内部错误' });
        }
    })


    app.post('/test',(req,res)=>{
        
        const token = req.headers.authorization;
        console.log(token);
        if(!token){
            res.status(401)
            return res.json({
                message: '未授权'
            })
        }
        try {
            // 验证 JWT
            const decoded = jwt.verify(token, secretKey);
            if(decoded){
                res.status(200)
                return res.json({
                    message: '授权成功',
                    status:200,
                    data:decoded
                })
            }
            res.status(500)
            return res.json({
                message: '未登录',
                status:500,
                data:decoded
            })

        }catch (error) {
            console.error(error);
            res.status(500).json({ message: '服务器内部错误' });
        }
    })
    
    app.listen(port, () => {
        console.log(`Server running on http://localhost:${port}`);
    });
})()
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <input type="text" name="" id="name">
    <input type="text" name="" id="pwd">
    <button id="reg">注册</button>
    <button id="login">登录</button>

    <button id="token">测试token</button>


    <script>
        document.querySelector('#reg').onclick = function(){
            let name = document.querySelector('#name').value;
            let pwd = document.querySelector('#pwd').value;
            // 1.创建ajax对象
            let xhr = new XMLHttpRequest();

            // 2.打开连接
            xhr.open('post', 'http://127.0.0.1:3000/register');
            xhr.setRequestHeader('Content-Type','application/json');

            // 3.发送请求
            xhr.send(JSON.stringify({
                username:name,
                password:pwd
            }));
            // 4.接收响应
            xhr.onreadystatechange = function(){
                if(xhr.readyState == 4 && xhr.status == 200){
                    let res = xhr.responseText;
                    if(res == 1){
                        alert('注册成功');
                    }else{
                        alert('注册失败');
                    }
                }
            }
        }

        document.querySelector('#login').onclick = function(){
            let name = document.querySelector('#name').value;
            let pwd = document.querySelector('#pwd').value;
            // 1.创建ajax对象
            let xhr = new XMLHttpRequest();

            // 2.打开连接
            xhr.open('post', 'http://127.0.0.1:3000/login');
            xhr.setRequestHeader('Content-Type','application/json');

            // 3.发送请求
            xhr.send(JSON.stringify({
                username:name,
                password:pwd
            }));
            // 4.接收响应
            xhr.onreadystatechange = function(){
                if(xhr.readyState == 4 && xhr.status == 200){
                    let res = xhr.responseText;

                    if(xhr.status ==200){
                        const {token} =JSON.parse(res);
                        localStorage.setItem('token',token);
                    }

                    // if(res == 1){
                    //     alert('注册成功');
                    // }else{
                    //     alert('注册失败');
                    // }
                }
            }
        }


        document.querySelector('#token').onclick = function(){
            // 1.创建ajax对象
            let xhr = new XMLHttpRequest();
            // 2.打开连接
            xhr.open('post', 'http://127.0.0.1:3000/test');
            xhr.setRequestHeader('Content-Type','application/json');
            
           let res = localStorage.getItem('token');
            if(res){
                xhr.setRequestHeader('Authorization',res);
            }            
            // 3.发送请求
            xhr.send(JSON.stringify({
     
            }));
            // 4.接收响应
            xhr.onreadystatechange = function(){
                if(xhr.readyState == 4 && xhr.status == 200){
                    let res = xhr.responseText;

                }
            }
        }

    </script>
</body>
</html>