基于 JSON Web Token 的身份认证系统

JSON Web Token

JSON Web Token 是一种基于 JSON 的开放标准(RFC 7519),用于在各个层面上向用户提供身份验证。

以下内容中将简称为 JWT 或 Token

JWT 由三部分组成:

  • Header
  • Payload
  • Signature

其中,Header 和 Payload 都是 JSON 对象,Signature 是 Header 和 Payload 的签名,用于验证 Token 的完整性。

Header 部分包含了 Token 的类型以及签名所用的算法。

json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}
ts 复制代码
export interface JWTHeader {
  alg: string;
  typ: string;
}

Payload

Payload 部分包含了用户的身份信息,例如用户 ID、用户名、权限等。

json 复制代码
{
  "userId": "1234567890",
  "username": "admin",
  "write": true,
  "exp": 1516239022
}
ts 复制代码
export interface JWTPayload {
  userId: string;   # 用户ID
  username: string; # 用户名
  write?: boolean;  # 管理界面权限
  exp?: number;     # 过期时间(暂未用到)
}

Signature

Signature 部分是 Header 和 Payload 的签名,用于验证 Token 的完整性。

ts 复制代码
function sign(payload: JWTPayload, secret: string): string {
  const header = { alg: "HS256", typ: "JWT" };

  const headerString = JSON.stringify(header);
  const payloadString = JSON.stringify(payload);
  const signatureString = crypto
    .createHmac("sha256", secret)
    .update(`${headerString}.${payloadString}`)
    .digest("hex");

  // 使用 `.` 拼接三个部分,这就是一个完整的 JSON Web Token
  return `${headerString}.${payloadString}.${signatureString}`;
}

验证 Token

ts 复制代码
function verify(token: string, secret: string): JWTPayload {
  const parts = token.split(".");

  const header = JSON.parse(Buffer.from(parts[0], "base64").toString());
  const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
  const signatureString = parts[2];

  // 验证算法
  if (header.alg !== "HS256") {
    throw new Error("无效算法");
  }

  // 验证签名
  // 通过对解析后的 header 和 payload 再次进行签名,比对两个签名是否一致
  const signature = crypto
    .createHmac("sha256", secret)
    .update(`${parts[0]}.${parts[1]}`)
    .digest("hex");
  if (signature !== signatureString) {
    throw new Error("无效签名");
  }

  // 验证过期时间
  if (payload.exp && payload.exp < Date.now()) {
    throw new Error("身份认证已过期");
  }

  return payload;
}

认证流程

1. 用户登录/注册

用户在登录/注册页面输入用户名和密码,点击登录/注册按钮,服务端将用户名和密码在数据库中进行验证/创建,验证通过后使用用户身份信息生成一个 Token,并将 Token 返回给客户端。

注意:Token 只应该包含可用与验证用户身份的信息,千万不要包含诸如密码之类的敏感信息,因为 Token 并不是加密的,在浏览器中使用函数 atob(token.split('.')[1]) 即可解析到 payload 对象的完整内容。

2. 客户端保存并使用 Token

客户端将 Token 保存在 Cookie 中。

客户端每次请求都会自动带上 Cookie,服务端可以通过验证 Cookie 中 Token 的有效性来判断用户的身份。

3. 服务端验证 Token

服务端首先检查请求的 Cookie 中是否有 Token,如果没有则返回错误信息。

如果有 Token,则通过解析 Token 验证它的有效性,没有通过验证则返回错误信息。

验证 Token 的有效性包括但不限于:

  • Token 是否被篡改
  • Token 是否过期

如果验证通过,则继续返回用户需要的资源。

权限控制

目前实现比较简单,通过解析 JWT Payload 对象,获取并校验 write 字段来判断用户是否有权限访问管理界面。

ts 复制代码
if (!payload.write) {
  // 处理没有权限的情况
}

常见问题

1. 如何防止 Token 被篡改?

在生成 JSON Web Token 时,服务端会使用一个只存在于服务端的密钥 secret 来生成每个 Token 的第三段的签名。

当客户端使用 Token 时,服务端会使用相同的密钥根据 Token 的第一段和第二段重新生成 Token 的签名,如果两个签名不一致,则说明 Token 被篡改了。

所以存在服务端的密钥 secret 很关键,一定不要泄漏它!

相关推荐
concisedistinct3 天前
全栈项目中是否可以实现统一错误处理链?如果可以,这条链路该如何设计?需要哪些技术支撑?是否能同时满足性能、安全性和用户体验需求?
软件开发·全栈
heroboyluck9 天前
rust 全栈应用框架dioxus server
rust·全栈·dioxus
灰飞肥鱼25 天前
DataGrip 查询TDengine时区问题
全栈
萌萌哒草头将军25 天前
🚀🚀🚀 神了!RedwoodJS 轻松碾压 NextJS,成了我的最爱❤️
前端·react.js·全栈
M1A11 个月前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈
小厂永远得不到的男人1 个月前
基于 Trae 的 WebSocket 聊天室保姆级教程(超详细版)
websocket·全栈·trae
ci0n1 个月前
PVE 网卡冗余配置
全栈
不想说话的麋鹿1 个月前
「项目实战」从0搭建NestJS后端服务(八):静态资源访问以及文件上传
前端·node.js·全栈
hboot1 个月前
rust 全栈应用框架dioxus
前端·rust·全栈
林夕11201 个月前
Node.js Web开发进阶:Stream、HTTP模块与文件上传全解析
前端·node.js·全栈