关注公众号:程序员老左,每天分享程序员职场经验及开发技术!
前言
在现代 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,查看其中的内容。)
总结一下
到这里希望大家能明白JWT 的主要目的是验证信息的真实性和完整性,而不是隐藏信息。它的文字流程我总结如下:
用户登录: 用户提交用户名和密码到服务器。
服务器验证: 服务器验证用户名和密码。
生成JWT: 验证成功后,服务器生成一个JWT,该Token包含了用户的标识信息,并通过密钥进行签名。
返回JWT: 服务器将JWT返回给客户端。
客户端存储JWT: 客户端收到JWT后,通常将其存储在LocalStorage或SessionStorage中。
请求携带JWT: 客户端在后续的每次请求中,都会在HTTP请求头(通常是Authorization字段)中携带JWT。
服务器验证JWT: 服务器收到请求后,验证JWT的有效性,包括签名、过期时间等。
处理请求: 验证通过后,服务器处理请求并返回响应。
END
看到这里你就算初步了解了JWT啦,可以在项目里面加上它啦,明白之后你还可以拿他去和传统的 Session + Cookie 认证机制进行对比,这里是全面理解两者的异同,为实际开发选择更合适的技术方案。
作者:大海是蓝色blue
链接:https://juejin.cn