前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JWT 鉴权机制</title>
<link
rel="stylesheet"
href="node_modules/bootstrap/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container clearfix">
<form class="mt-5">
<div class="mb-3">
<label class="form-label">用户名</label>
<input
type="text"
class="form-control"
name="username"
value="zhangsan"
/>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input
type="password"
class="form-control"
name="password"
value="123456"
/>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
<button type="button" class="btn btn-secondary mt-5" id="protectedBtn">
访问受保护的资源
</button>
</div>
<script src="node_modules/axios/dist/axios.min.js"></script>
<script>
// 获取表单 DOM 对象
const form = document.querySelector("form");
// 获取受保护资源按钮 DOM 对象
const protectedBtn = document.querySelector("#protectedBtn");
// 监听表单的提交事件
form.addEventListener("submit", async (e) => {
// 阻止表单默认提交行为
e.preventDefault();
// 获取表单数据
const formData = new FormData(form);
const username = formData.get("username");
const password = formData.get("password");
try {
// 发送 POST 请求到登录接口
const response = await axios.post("http://localhost:3000/login", {
username,
password,
});
// 将 token 存储在 localStorage
localStorage.setItem("accessToken", response.data.accessToken);
localStorage.setItem("refreshToken", response.data.refreshToken);
// 可以重定向到其他页面或显示登录成功信息
alert("登录成功!");
} catch (error) {
// 处理错误,例如显示登录失败信息
alert(error.response.data.message);
}
});
// 监听按钮的点击事件
protectedBtn.addEventListener("click", async () => {
try {
// 从 localStorage 获取 accessToken
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
alert("未登录或登录已过期,请重新登录获取访问令牌。");
return;
}
// 发送 GET 请求到受保护的资源接口
const response = await axios.get("http://localhost:3000/protected", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// 显示受保护资源的数据
alert(response.data.message);
} catch (error) {
if (error.response && error.response.status === 401) {
// 如果访问令牌过期,尝试使用刷新令牌获取新的访问令牌
try {
const refreshToken = localStorage.getItem("refreshToken");
const refreshResponse = await axios.post(
"http://localhost:3000/refresh",
{
refreshToken,
}
);
localStorage.setItem(
"accessToken",
refreshResponse.data.accessToken
);
localStorage.setItem(
"refreshToken",
refreshResponse.data.refreshToken
);
// 重新尝试原来的请求
const retryResponse = await axios.get(
"http://localhost:3000/protected",
{
headers: {
Authorization: `Bearer ${refreshResponse.data.accessToken}`,
},
}
);
alert(retryResponse.data.message);
} catch (refreshError) {
alert("无法刷新令牌,请重新登录。");
}
} else {
alert(error.response.data.message);
}
}
});
</script>
</body>
</html>
后端
// 引入必要的模块
const express = require("express");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const cors = require("cors");
// 创建 Express 应用
const app = express();
const PORT = 3000;
// 定义 JWT 密钥
const SECRET_KEY = "your_secret_key";
const REFRESH_SECRET_KEY = "your_refresh_secret_key";
// 允许跨域访问
app.use(cors());
// 使用 bodyParser 中间件来解析 JSON 请求体
app.use(bodyParser.json());
// 用户数据,通常应该存储在数据库中
const users = [
{
id: 1,
username: "zhangsan",
password: bcrypt.hashSync("123456", 10),
},
];
// 登录接口
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = users.find((u) => u.username === username);
if (!user) {
return res.status(401).json({ message: "登录失败1" });
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: "登录失败2" });
}
const accessToken = jwt.sign({ id: user.id }, SECRET_KEY, {
expiresIn: "15m",
});
const refreshToken = jwt.sign({ id: user.id }, REFRESH_SECRET_KEY, {
expiresIn: "7d",
});
res.json({ accessToken, refreshToken });
});
// 刷新令牌接口
app.post("/refresh", (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET_KEY);
const newAccessToken = jwt.sign({ id: decoded.id }, SECRET_KEY, {
expiresIn: "15m",
});
const newRefreshToken = jwt.sign({ id: decoded.id }, REFRESH_SECRET_KEY, {
expiresIn: "7d",
});
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
} catch (error) {
return res.status(401).json({ message: "刷新令牌失败" });
}
});
// JWT 验证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (token == null) return res.sendStatus(401);
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err)
return res.status(401).json({ message: "未能成功访问到受保护的资源" });
req.user = user;
next();
});
}
// 受保护的资源接口
app.get("/protected", authenticateToken, (req, res) => {
res.json({
message: `欢迎, 你的 ID 是 ${req.user.id}. 你正在访问受保护的资源`,
});
});
// 启动服务器
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});