项目必备点-JWT鉴权你真的弄明白了吗?
2025-02-17 09:37 阅读(64)

关注公众号:程序员老左,每天分享程序员职场经验及开发技术!

前言

在现代 Web 开发中,用户身份验证是每个项目不可或缺的一部分。JWT(JSON Web Token)作为一种轻量级、跨语言的身份验证解决方案,因其简单易用和高效性而备受开发者青睐。然而,在实际项目中,很多人对 JWT 的使用方式以及其背后的原理并不完全理解,直接就是照搬着用,这其实是治标不治本的!

JWT 是什么?

首先我们需要知道的就是JWT究竟是个什么样的玩意,JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它通常被用来进行用户认证和信息交换。

JWT 的三个组成部分

1. 头部(Headers)

头部通常包含两部分信息:令牌的类型(通常是 JWT)和使用的签名算法,例如 HMAC SHA256 或 RSA。它会被编码成 JSON 对象,然后使用 Base64Url 编码得到第一部分。示例如下:

{
  "alg": "HS256",
  "typ": "JWT"
}

2. 载荷(Payload)

载荷包含声明(Claims),声明是关于实体(通常是用户)和其他数据的声明。声明分为三种类型:


注册声明:如 iss(发行人)、sub(主题)、aud(受众)等。


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


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


在代码中,payload 部分是我们手动定义的,例如:

const payload = {
    username: username
};

3. 签名(Signature)

为了创建签名部分,你必须使用编码后的头部、编码后的载荷、一个密钥(secretKey)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按以下方式创建:


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

知道了它是什么之后,就方便我们更深一步的了解


如何在你的项目里面使用JWT

我们这里使用的是node的框架KOA + jsonwebtoken库来实现 JWT 签名和鉴权接口,同时配置好 token 的加密信息。

1. 初始化项目

首先创建项目目录并初始化 package.json 文件:

mkdir koa-jwt-example
cd koa-jwt-example
npm init -y

2. 安装依赖

安装所需的依赖包,包括 koa、koa-bodyparser(用于解析请求体)和 jsonwebtoken:

npm install koa koa-bodyparser jsonwebtoken

这里我特别解释一下 为什么我们需要koa-bodyparser


这是因为在 Koa 应用中,中间件是按照注册的顺序依次执行的。通过 app.use(bodyParser()); 将 koa - bodyparser 中间件注册到应用中,意味着在处理每个请求时,都会先经过 koa - bodyparser 中间件的处理。这样,后续的路由处理函数就可以直接从 ctx.request.body 中获取解析好的请求体数据,而无需手动处理请求体的解析逻辑,从而简化了开发过程,提高了代码的可维护性。

2. 接下来直接上代码

每一行代码我都写好了注释,都值得大家去细细品味!

// 引入 Koa 框架,用于构建 Web 应用程序
const Koa = require('koa');
// 引入 koa-bodyparser 中间件,用于解析请求体中的数据,支持多种格式,如 JSON、表单等
const bodyParser = require('koa-bodyparser');
// 引入 jsonwebtoken 库,用于生成和验证 JSON Web Token(JWT)
const jwt = require('jsonwebtoken');

// 创建一个 Koa 应用实例
const app = new Koa();
// 定义服务器监听的端口号
const port = 3000;

// 配置 token 的加密信息,这个密钥用于 JWT 的签名和验证,应该妥善保管,不能泄露
const secretKey = 'yourSecretKey';

// 使用 bodyParser 中间件,使得 Koa 应用可以解析请求体中的数据
// 这样在后续的请求处理中,可以直接通过 ctx.request.body 获取请求体中的数据
app.use(bodyParser());

// 定义一个中间件函数,用于处理不同路径和方法的请求
app.use(async (ctx) => {
    // 检查请求的路径是否为 /login 且请求方法是否为 POST
    if (ctx.path === '/login' && ctx.method === 'POST') {
        // 从请求体中解构出用户名和密码
        const { username, password } = ctx.request.body;
        // 模拟用户信息验证,实际应用中应该从数据库中查询并验证用户信息
        if (username === 'admin' && password === 'password') {
            // 如果验证通过,创建一个包含用户名的载荷对象,该对象将被包含在 JWT 中
            const payload = {
                username: username
            };
            // 使用 jwt.sign 方法生成 JWT
            // 第一个参数是载荷对象,第二个参数是加密密钥,第三个参数设置 JWT 的过期时间为 1 小时
            const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
            // 将包含 token 的对象作为响应体返回给客户端
            ctx.body = { token };
        } else {
            // 如果验证失败,设置响应状态码为 401,表示未授权
            ctx.status = 401;
            // 将包含错误信息的对象作为响应体返回给客户端
            ctx.body = { message: 'Invalid credentials' };
        }
    // 检查请求的路径是否为 /protected 且请求方法是否为 GET
    } else if (ctx.path === '/protected' && ctx.method === 'GET') {
        // 从请求头中获取 Authorization 字段的值
        const authHeader = ctx.headers['authorization'];
        // 从 Authorization 字段的值中提取出 JWT
        // 通常 Authorization 字段的值格式为 "Bearer <token>",所以使用 split(' ') 分割并取第二个元素
        const token = authHeader && authHeader.split(' ')[1];
        // 检查是否提供了 token
        if (!token) {
            // 如果没有提供 token,设置响应状态码为 401,表示未授权
            ctx.status = 401;
            // 将包含错误信息的对象作为响应体返回给客户端
            ctx.body = { message: 'No token provided' };
            // 终止当前中间件的执行
            return;
        }

        try {
            // 使用 jwt.verify 方法验证 token 的有效性
            // 如果验证通过,将解析出的用户信息存储在 user 变量中
            const user = jwt.verify(token, secretKey);
            // 将包含成功信息和用户信息的对象作为响应体返回给客户端
            ctx.body = { message: 'This is a protected route', user };
        } catch (err) {
            // 如果验证失败,设置响应状态码为 403,表示禁止访问
            ctx.status = 403;
            // 将包含错误信息的对象作为响应体返回给客户端
            ctx.body = { message: 'Invalid token' };
        }
    }
});

// 启动服务器,监听指定的端口号
// 当服务器启动成功后,在控制台输出提示信息
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里我怕大家会有疑惑,为什么下面这段代码就生成了JWT,除了payload在里面,我也没看到头部和签名呀


const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });

在上述代码里,我们使用 jwt.sign 方法生成 JWT,虽然没有显式地定义头部,但 jsonwebtoken 库会自动处理头部的生成和编码:

jwt.sign 方法会在内部完成以下操作

生成默认的头部,通常包含 alg(签名算法)和 typ(令牌类型)。

对头部和载荷进行 Base64Url 编码。

使用指定的签名算法和密钥对编码后的头部和载荷进行签名。

将编码后的头部、载荷和签名用 . 连接起来,形成最终的 JWT。


所以,虽然代码中没有显式地处理 JWT 的三个部分,但 jsonwebtoken 库会自动完成这些工作,我们只需要提供必要的信息即可生成完整的 JWT。

需要注意的点

密钥管理:

JWT_SECRET 是生成和验证 Token 的核心密钥,请妥善保管。

在生产环境中,建议将密钥存储在环境变量中,而不是直接写在代码中。(安装dotenv,配置在.env后缀文件中)


Token 过期时间:


设置合理的过期时间(如 15 分钟或 1 小时),并通过刷新机制延长用户的会话时间。


安全性:


始终通过 HTTPS 传输 Token,防止被窃听或篡改。

不要在 Token 的载荷中存储敏感信息。(虽然 JWT 的签名可以防止 Token 被篡改,但 Base64 URL 编码并不是加密。任何人都可以通过工具轻松解码 JWT 的 Header 和 Payload,查看其中的内容。)

https://www.zuocode.com

总结一下

到这里希望大家能明白JWT 的主要目的是验证信息的真实性和完整性,而不是隐藏信息。它的文字流程我总结如下:


用户登录: 用户提交用户名和密码到服务器。


服务器验证: 服务器验证用户名和密码。


生成JWT: 验证成功后,服务器生成一个JWT,该Token包含了用户的标识信息,并通过密钥进行签名。


返回JWT: 服务器将JWT返回给客户端。


客户端存储JWT: 客户端收到JWT后,通常将其存储在LocalStorage或SessionStorage中。


请求携带JWT: 客户端在后续的每次请求中,都会在HTTP请求头(通常是Authorization字段)中携带JWT。


服务器验证JWT: 服务器收到请求后,验证JWT的有效性,包括签名、过期时间等。


处理请求: 验证通过后,服务器处理请求并返回响应。


END

看到这里你就算初步了解了JWT啦,可以在项目里面加上它啦,明白之后你还可以拿他去和传统的 Session + Cookie 认证机制进行对比,这里是全面理解两者的异同,为实际开发选择更合适的技术方案。


作者:大海是蓝色blue

链接:https://juejin.cn