node.js基础学习-JWT登录鉴权(十四)

发布于:2024-12-06 ⋅ 阅读:(98) ⋅ 点赞:(0)

一、前言

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它本质上是一个字符串,由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。这三个部分通过 “.” 连接,形成一个完整的 JWT,例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

二、JWT组成部分

  • 头部(Header)

  • 主要包含两部分信息,令牌的类型(通常是 “JWT”)和所使用的签名算法(如 HMAC SHA256、RSA 等)。例如,一个典型的头部可能是{"alg": "HS256", "typ": "JWT"},这个头部经过 Base64Url 编码后就成为 JWT 的第一部分。

  • 作用是告诉接收方这个令牌的基本信息,包括如何验证它的签名。

  • 载荷(Payload)

  • 这部分包含了实际要传输的信息,也被称为声明(Claims)。这些声明可以是预定义的(如 “iss” - 令牌发行者、“sub” - 主题、“aud” - 受众、“exp” - 过期时间、“nbf” - 在此日期之前不可用),也可以是自定义的。例如,{"sub": "1234567890", "name": "John Doe", "iat": 1516239022},这里的 “sub” 可能代表用户的唯一标识,“name” 是用户名称,“iat” 是令牌发布时间。

  • 经过 Base64Url 编码后成为 JWT 的第二部分,用于在不同系统之间传递一些必要的用户或业务相关的数据,同时不会像在纯文本中那样容易被篡改。

  • 签名(Signature)

  • 签名是通过对头部和载荷进行特定算法运算得到的。首先将头部和载荷分别进行 Base64Url 编码,然后使用头部中指定的签名算法(如 HS256),结合一个只有服务器知道的密钥(Secret),对编码后的头部和载荷进行签名计算。例如,对于 HS256 算法,签名计算公式可能是HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

  • 签名的作用是确保 JWT 的完整性和真实性。接收方可以使用相同的算法和密钥来验证签名,从而判断 JWT 在传输过程中是否被篡改。如果签名验证失败,那么这个 JWT 就应该被拒绝。

三、JWT登录鉴权实现过程

1、前端实现过程

1.安装 Axios

在前端项目的根目录下,打开终端并执行 npm install axios 命令。

2. 配置 Axios 拦截器

请求拦截器(添加 JWT 到请求头)
在 JavaScript 文件(例如 axiosConfig.js)中,引入 Axios:

 import axios from 'axios';

然后设置请求拦截器:

     axios.interceptors.request.use((config) => {
         const token = localStorage.getItem('jwtToken');
         if (token) {
             // 如果存在 JWT 令牌,将其添加到请求头的 Authorization 字段,格式为 Bearer <令牌>
             config.headers.Authorization = `Bearer ${token}`;
         }
         // 返回修改后的配置对象,以便 Axios 使用该配置发送请求
         return config;
     }, (error) => {
         // 如果在设置请求头过程中出现错误,直接拒绝该 Promise,将错误传递给调用者
         return Promise.reject(error);
     });

响应拦截器(处理 JWT 相关错误)
继续在 axiosConfig.js 文件中设置响应拦截器:

     axios.interceptors.response.use((response) => {
         // 如果响应成功(状态码在 200 - 299 之间),直接返回响应数据
         return response;
     }, (error) => {
         // 检查响应的状态码,如果存在的话
         const status = error.response? error.response.status : null;
         if (status === 401 || status === 403) {
             // 如果状态码是 401(未授权)或 403(禁止访问),表示 JWT 可能过期或无效
             // 清除本地存储中的 JWT 令牌
             localStorage.removeItem('jwtToken');
             // 重定向到登录页面(这里假设使用 React Router,并且已经在组件中获取了路由历史对象 history)
             const history = useHistory();
             history.push('/login');
         }
         // 将错误重新抛出,以便在调用处进行处理
         return Promise.reject(error);
     });

3.登录获取 JWT 并存储
在登录组件(例如 Login.js)中,当用户点击登录按钮后,收集用户名和密码,然后使用 Axios 发送 POST 请求到后端的登录接口:

   import axios from 'axios';
   async function login(username, password) {
       try {
           const response = await axios.post('http://localhost:3000/login', {
               username: username,
               password: password
           });
           const token = response.data.token;
           // 将 JWT 存储在本地存储中
           localStorage.setItem('jwtToken', token);
           return token;
       } catch (error) {
           console.error('登录出错:', error);
           throw error;
       }
   }

4.处理 JWT 过期自动刷新(可选)

可以设置一个定时器来检查 JWT 的过期时间,并在即将过期时尝试刷新令牌。以下是一个简单示例:

   function checkTokenExpiration() {
       const token = localStorage.getItem('jwtToken');
       if (token) {
           const decodedToken = jwt_decode(token);
           const expirationTime = decodedToken.exp * 1000; // 将过期时间转换为毫秒
           const currentTime = Date.now();
           const timeToExpiration = expirationTime - currentTime;
           // 假设在令牌过期前 5 分钟进行刷新操作
           if (timeToExpiration < 5 * 60 * 1000) {
               refreshToken();
           }
       }
   }
   async function refreshToken() {
       try {
           const refreshToken = localStorage.getItem('refreshToken');
           const response = await axios.post('http://localhost:3000/refresh-token', {
               refreshToken: refreshToken
           });
           const newToken = response.data.token;
           localStorage.setItem('jwtToken', newToken);
       } catch (error) {
           console.error('刷新令牌出错:', error);
           // 如果刷新令牌失败,可能需要清除令牌并跳转到登录页面
           localStorage.removeItem('jwtToken');
           const history = useHistory();
           history.push('/login');
       }
   }
   // 每隔一段时间检查令牌过期情况
   setInterval(checkTokenExpiration, 60 * 1000); 

这里假设后端有一个 refresh-token 接口用于刷新令牌,并且前端存储了一个 refreshToken 用于刷新操作。同时,使用 jwt_decode 库(需要先安装)来解析 JWT 获取过期时间信息。

5.使用 Axios 发送请求(以获取用户信息为例)

在需要获取用户信息的组件(例如 UserProfile.js)中,引入 Axios:

   import axios from 'axios';

然后发送请求:

   axios.get('http://localhost:3000/api/user-info')
 .then((response) => {
         // 如果请求成功,打印用户信息
         console.log('用户信息:', response.data);
     })
 .catch((error) => {
         // 如果请求失败,打印错误信息
         console.error('获取用户信息出错:', error);
     });

2、后端实现过程

1.安装相关库(以 Node.js 为例):

在后端项目的根目录下,打开终端并执行 npm install jsonwebtoken 命令。

2.设置 JWT 密钥(环境变量):

在开发环境中,对于 Linux 或 macOS 系统,可以在终端中设置环境变量:

   export JWT_SECRET=mysecretkey

对于 Windows 系统,可以使用:

   set JWT_SEAST=mysecretkey

在生产环境中,通常会使用服务器的环境变量配置功能来设置 JWT_SECRET

在后端代码(例如 app.js)中获取密钥:

   const jwtSecret = process.env.JWT_SECRET;

3.用户登录接口(生成 JWT)

假设后端有一个用户模型(例如使用 Mongoose 定义的用户模型),用于验证用户凭据。以下是一个简单的用户登录接口,用于生成 JWT:

   const express = require('express');
   const jwt = require('jsonwebtoken');
   const User = require('./models/user');
   const app = express();
   app.post('/login', async (req, res) => {
       const {username, password} = req.body;
       try {
           const user = await User.findOne({username, password});
           if (!user) {
               res.status(401).send('用户名或密码错误');
           } else {
               const token = jwt.sign({id: user._id, username: user.username}, jwtSecret, {expiresIn: '1h'});
               // 生成刷新令牌(可以是一个随机字符串等)
               const refreshToken = generateRefreshToken(); 
               res.send({token, refreshToken});
           }
       } catch (err) {
           console.error('登录出错:', err);
           res.status(500).send('内部错误');
       }
   });
   function generateRefreshToken() {
       // 这里简单返回一个随机字符串作为示例,实际应用中可以更复杂的生成逻辑
       return Math.random().toString(36).substring(2, 15);
   }

这个接口首先从请求体中获取用户名和密码,然后在数据库中查找匹配的用户(这里假设 User.findOne 方法用于在数据库中查找用户)。如果找到用户,就使用 jwt.sign 方法生成一个 JWT,有效期为 1 小时,并生成一个刷新令牌,然后将它们发送给前端;如果找不到用户或者出现错误,就返回相应的错误信息。

4.验证 JWT 中间件(保护路由):

为了保护需要认证的后端路由,需要创建一个中间件来验证 JWT。以下是一个简单的中间件示例:

   function authenticateToken(req, res, next) {
       const authHeader = req.headers['authorization'];
       const token = authHeader && authHeader.split(' ')[1];
       if (token == null) {
           return res.status(401).send('未提供认证令牌');
       }
       jwt.verify(token, jwtSecret, (err, user) => {
           if (err) {
               return res.status(403).send('无效的认证令牌');
           }
           req.user = user;
           next();
       });
   }

这个中间件首先从请求头中提取 JWT,然后使用 jwt.verify 方法来验证 JWT 的有效性。如果 JWT 有效,就将用户信息(从 JWT 的载荷部分解析出来的)存储在 req.user 中,并调用 next 函数,允许请求继续处理;如果 JWT 无效或者未提供,就返回相应的错误信息。可以将这个中间件应用到需要认证的路由上,例如:

   app.get('/api/user-info', authenticateToken, (req, res) => {
       res.send('这是一个受保护的路由,只有通过认证的用户才能访问');
   });

当用户访问 /api/user-info 路由时,首先会经过 authenticateToken 中间件进行 JWT 验证,只有验证通过的用户才能访问该路由的内容。

5.刷新令牌接口(可选)

如果前端实现了令牌刷新功能,后端需要提供一个刷新令牌的接口:

   app.post('/refresh-token', (req, res) => {
       const {refreshToken} = req.body;
       // 这里需要验证刷新令牌的有效性,例如检查是否在数据库中存在且未过期等
       if (isRefreshTokenValid(refreshToken)) {
           const user = getUserFromRefreshToken(refreshToken);
           const newToken = jwt.sign({id: user._id, username: user.username}, jwtSecret, {expiresIn: '1h'});
           res.send({token: newToken});
       } else {
           res.status(401).send('无效的刷新令牌');
       }
   });
   function isRefreshTokenValid(refreshToken) {
       // 简单示例,实际应用中需要更完善的验证逻辑
       return refreshToken.length > 0;
   }
   function getUserFromRefreshToken(refreshToken) {
       // 假设根据刷新令牌可以获取到对应的用户信息,这里简单返回一个示例用户
       return {id: 1, username: 'exampleUser'};
   }

这个接口接收前端发送的刷新令牌,验证其有效性,如果有效则生成一个新的 JWT 并返回给前端;如果无效则返回 401 错误。

通过以上前后端的实现过程,不仅实现了基本的 JWT 认证,还包含了令牌生成、存储、过期处理以及刷新等功能,能够构建一个相对完整的用户认证系统。在实际应用中,还需要根据项目的具体需求进一步优化和完善代码,例如加强令牌的安全性、完善数据库操作等。