发布于 2026-01-06 0 阅读
0

在 Node 和 Express.js 中使用 JWT 进行身份验证和授权 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

在 Node 和 Express.js 中使用 JWT 进行身份验证和授权

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

在本教程中,我们将学习如何使用 JWT 为 Nodejs 和 Express 应用程序构建身份验证系统。

我们将使用Node、Express、MongoDB和Docker来完成本教程中的项目,构建一个API。您可以在这里找到本教程的代码源代码

什么是身份验证和授权?

简单来说,身份验证就是核实某人身份的过程。

授权是验证用户可以访问哪些数据的过程。

只有在您通过身份验证后,才会进行授权。之后,系统会授予您访问所需文件的权限。

设置项目

首先,克隆该项目。

git clone https://github.com/koladev32/node-docker-tutorial.git
Enter fullscreen mode Exit fullscreen mode

完成后,进入项目并运行。

yarn install
Enter fullscreen mode Exit fullscreen mode

使用以下命令启动项目:

yarn start
Enter fullscreen mode Exit fullscreen mode

在项目根目录下创建一个.env文件。

// .env
JWT_SECRET_KEY=)a(s3eihu+iir-_3@##ha$r$d4p5%!%e1==#b5jwif)z&kmm@7
Enter fullscreen mode Exit fullscreen mode

您可以在这里轻松地在线生成此密钥的新值

创建用户模型

我们来创建用户模型。但首先,我们需要为这个模型定义一个类型。

// src/types/user.ts
import { Document } from "mongoose";

export interface IUser extends Document {
  username: string;
  password: string;
  isAdmin: boolean;
}
Enter fullscreen mode Exit fullscreen mode

太好了,那我们就可以编写用户模型了。

// src/models/user.ts

import { IUser } from "../types/user";
import { model, Schema } from "mongoose";

const userSchema: Schema = new Schema(
  {
    username: {
      type: String,
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    isAdmin: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
  { timestamps: true }
);

export default model<IUser>("user", userSchema);
Enter fullscreen mode Exit fullscreen mode

用户模型已创建完成。我们可以开始编写登录和注册控制器了。

登记

进入该controllers目录并创建一个新目录,users该目录将包含一个新index.ts文件。

让我们来编写registerUser控制器。

// src/controllers/users/index.ts

import { Response, Request } from "express";
import { IUser } from "../../types/user";
import User from "../../models/user"
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
let refreshTokens: string[] = [];

const registerUser = async (
  req: Request,
  res: Response
): Promise<e.Response<any, Record<string, any>>> => {
  try {
    const { username, password } = req.body;
    if (!(username && password)) {
      return res.status(400).send("All inputs are required");
    }

    // Checking if the user already exists

    const oldUser = await User.findOne({ username });

    if (oldUser) {
      return res.status(400).send("User Already Exist. Please Login");
    }

    const user: IUser = new User({
      username: username,
    });

    const salt = await bcrypt.genSalt(10);
    // now we set user password to hashed password
    user.password = await bcrypt.hash(password, salt);

    user.save().then((doc) => {
      // Generating Access and refresh token
      const token = jwt.sign(
        { user_id: doc._id, username: username },
        process.env.JWT_SECRET_KEY,
        {
          expiresIn: "5min",
        }
      );

      const refreshToken = jwt.sign(
        { user_id: doc._id, username: username },
        process.env.JWT_SECRET_KEY
      );

      refreshTokens.push(refreshToken);

      return res.status(201).json({
        user: doc,
        token: token,
        refresh: refreshToken,
      });
    });

    return res.status(400).send("Unable to create user");
  } catch (error) {
    throw error;
  }
};

export {registerUser};
Enter fullscreen mode Exit fullscreen mode

我们在这里做什么?

  • 请检查是否已提供所有必填字段。
  • 检查是否存在同名用户
  • 创建用户并加密密码
  • 生成刷新令牌和访问令牌
  • 回复

但为什么需要刷新和访问令牌?

当令牌过期时,最直观的获取新令牌的方法是重新登录。但这对于用户体验来说效率很低。
因此,客户端无需重新登录,而是可以使用登录或注册时获得的刷新令牌发送请求来获取新的访问令牌。
我们稍后会编写相关的路由。

现在,让我们将此控制器添加到路由中,并在我们的应用程序中注册新路由。


// src/routes/index.ts

import { Router } from "express";
import {
  getMenus,
  addMenu,
  updateMenu,
  deleteMenu,
  retrieveMenu,
} from "../controllers/menus";
import {
  registerUser
} from "../controllers/users";

const menuRoutes: Router = Router();

const userRoutes: Router = Router();

// Menu Routes

menuRoutes.get("/menu", getMenus);
menuRoutes.post("/menu", addMenu);
menuRoutes.put("/menu/:id", updateMenu);
menuRoutes.delete("/menu/:id", deleteMenu);
menuRoutes.get("/menu/:id", retrieveMenu);

// User Routes

userRoutes.post("/user/register", registerUser);

export { menuRoutes, userRoutes };
Enter fullscreen mode Exit fullscreen mode

app.ts文件中,我们使用新的路由。

// src/app.ts

import { menuRoutes, userRoutes } from "./routes";
...
app.use(cors());
app.use(express.json());

app.use(userRoutes);
...
Enter fullscreen mode Exit fullscreen mode

该端点位于 localhost:4000/user/register。

失眠登记

登录

在用户控制器文件中index.ts,我们来编写登录函数。

// src/controllers/users/index.ts

const loginUser = async (
  req: Request,
  res: Response
): Promise<e.Response<any, Record<string, any>>> => {
  try {
    const { username, password } = req.body;
    if (!(username && password)) {
      return res.status(400).send("All inputs are required");
    }

    // Checking if the user exists

    const user: IUser | null = await User.findOne({ username });

    if (user && (await bcrypt.compare(password, user.password))) {
      // Create token
      const token = jwt.sign(
        { user_id: user._id, username: username },
        process.env.JWT_SECRET_KEY,
        {
          expiresIn: "5min",
        }
      );

      const refreshToken = jwt.sign(
        { user_id: user._id, username: username },
        process.env.JWT_SECRET_KEY
      );

      refreshTokens.push(refreshToken);

      // user
      return res.status(200).json({
        user: user,
        token: token,
        refresh: refreshToken,
      });
    }

    return res.status(400).send("Invalid Credentials");
  } catch (error) {
    throw error;
  }
};

export { registerUser, loginUser };
Enter fullscreen mode Exit fullscreen mode

我们在这里做什么?

  • 请检查是否已提供所有必填字段。
  • 检查用户是否存在
  • 比较密码,如果一切正确,则创建新令牌。
  • 然后发送回复

如果这些验证未完成,我们也会发送错误消息。

将其添加到路由中,然后使用https://localhost:4500/user/login登录。

// src/routes/index.ts

...
userRoutes.post("/user/login", loginUser);
...
Enter fullscreen mode Exit fullscreen mode

保护菜单资源

太好了。登录端点和注册端点都完成了。但是资源没有受到保护。
你仍然可以访问它们,因为我们需要编写一个中间件。

中间件是一种函数,它充当请求和执行请求的函数之间的桥梁。

创建一个名为middleware“内部”的新目录src,并创建一个文件index.ts

太好了,我们来编写中间件。

// src/middleware/index.ts

import e, { Response, Request, NextFunction } from "express";
import { IUser } from "../types/user";

const jwt = require("jsonwebtoken");

const authenticateJWT = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<e.Response<any, Record<string, any>>> => {
  const authHeader = req.headers.authorization;

  if (authHeader) {
    const [header, token] = authHeader.split(" ");

    if (!(header && token)) {
      return res.status(401).send("Authentication credentials are required.");
    }

    jwt.verify(token, process.env.JWT_SECRET_KEY, (err: Error, user: IUser) => {
      if (err) {
        return res.sendStatus(403);
      }

      req.user = user;
      next();
    });
  }
  return res.sendStatus(401);
};

export default authenticateJWT;
Enter fullscreen mode Exit fullscreen mode

我们在这里做什么?

  • 确保存在授权标头。我们希望此标头的值采用以下格式:'Bearer Token'。
  • 验证令牌,然后创建一个新密钥,其user值为该令牌。req.user = user
  • 最后,使用 ` next()use` 函数执行下一个函数。

现在,让我们在应用程序中使用中间件。

// src/app.ts

import authenticateJWT from "./middleware";
...

app.use(userRoutes);

app.use(authenticateJWT);

app.use(menuRoutes);
...
Enter fullscreen mode Exit fullscreen mode

你注意到什么了吗?中间件放在了 `<head>` 标签之后、` userRoutes<head>` 标签之前menuRoutes
这样一来,Node 和 Express 就能识别出 `<head>` 标签userRoutes未受保护,并且 `<head>` 标签之后的所有路由都authenticateJWT需要访问令牌。

为了测试这一点,请向http://localhost:4000/menusGET发送一个不带授权标头的请求。您会收到一个错误。 然后使用您上次登录获得的访问令牌,并将其添加到授权标头中。 您应该可以检索到菜单。401

刷新令牌

现在该编写刷新令牌控制器了。

// src/controllers/users/index.ts

const retrieveToken = async (
  req: Request,
  res: Response
): Promise<e.Response<any, Record<string, any>>> => {
  try {
    const { refresh } = req.body;
    if (!refresh) {
      return res.status(400).send("A refresh token is required");
    }

    if (!refreshTokens.includes(refresh)) {
      return res.status(403).send("Refresh Invalid. Please login.");
    }

    jwt.verify(
      refresh,
      process.env.JWT_SECRET_KEY,
      (err: Error, user: IUser) => {
        if (err) {
          return res.sendStatus(403);
        }

        const token = jwt.sign(
          { user_id: user._id, username: user.username },
          ")a(s3eihu+iir-_3@##ha$r$d4p5%!%e1==#b5jwif)z&kmm@7",
          {
            expiresIn: "5min",
          }
        );

        return res.status(201).send({
          token: token,
        });
      }
    );

    return res.status(400).send("Invalid Credentials");
  } catch (error) {
    throw error;
  }
};
Enter fullscreen mode Exit fullscreen mode

我们在这里做什么?

  • 确保刷新令牌存在于请求体中
  • 确保刷新令牌存在于服务器内存中
  • 最后验证刷新令牌,然后发送新的访问令牌。

将此新控制器添加到userRoutes

// src/routes/index.ts
...
userRoutes.post("/user/refresh", retrieveToken);
...
Enter fullscreen mode Exit fullscreen mode

您可以访问http://localhost:4000/user/refresh获取新的访问令牌。

失眠刷新代币

注销

但这里有个问题。如果用户的刷新令牌被盗,其他人就可以用它生成任意数量的新令牌。我们得让它失效。

// src/controllers/users/index.ts
...
const logoutUser = async (
  req: Request,
  res: Response
): Promise<e.Response<any, Record<string, any>>> => {
  try {
    const { refresh } = req.body;
    refreshTokens = refreshTokens.filter((token) => refresh !== token);

    return res.status(200).send("Logout successful");
  } catch (error) {
    throw error;
  }
};

export { registerUser, loginUser, retrieveToken, logoutUser };
Enter fullscreen mode Exit fullscreen mode

以及一条新的登出路线。

// src/routes/index.ts

import {
  loginUser,
  logoutUser,
  registerUser,
  retrieveToken,
} from "../controllers/users";
...
userRoutes.post("user/logout", logoutUser);
...
Enter fullscreen mode Exit fullscreen mode

您可以访问http://localhost:4000/user/logout来使令牌失效。

好了,大功告成!🥳

结论

在本文中,我们学习了如何使用 JWT 为我们的 Node 和 Express 应用程序构建身份验证系统。

每篇文章都有改进的空间,所以欢迎在评论区提出您的建议或问题。😉

请点击此处查看本教程的代码

文章来源:https://dev.to/koladev/authentication-and-authorization-with-jwts-in-node-expressjs-5a9a