一、前言
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 认证,还包含了令牌生成、存储、过期处理以及刷新等功能,能够构建一个相对完整的用户认证系统。在实际应用中,还需要根据项目的具体需求进一步优化和完善代码,例如加强令牌的安全性、完善数据库操作等。