php中文网

在 Nodejs 中进行身份验证的正确方法 [uide]

php中文网

身份验证是后端开发中最关键但经常被误解的方面之一。由于其复杂性,开发人员经常转向第三方解决方案,例如 auth0 或 supabase。虽然这些都是优秀的工具,但构建您自己的身份验证系统可以提供更大的灵活性和控制力。

在本指南中,您将了解如何以最少的依赖关系为 express.js api 服务实现简单的身份验证中间件。到最后,您将拥有:

  • 功能齐全的用户名 密码身份验证。
  • 与 postgresql 集成来存储用户帐户。
  • 基于 jwt 的身份验证中间件。
  • 通过自动重用检测刷新令牌以增强安全性。

本指南注重简单性,避免使用诸如 passport.js 之类的包来降低复杂性。


设置用户帐户表

首先,创建一个 postgresql 表来存储用户帐户:

create table users (
    "id" serial primary key,
    "username" varchar(255) unique not null,
    "password" varchar(255) not null,
    "email" varchar(255) unique,
    "created_at" timestamp not null default now()
);

jwt 身份验证中间件

接下来,创建 jwt 身份验证中间件来保护 api 端点。此示例使用对称加密。对于微服务架构,请考虑使用带有公钥/私钥对的非对称加密。

中间件代码(/src/middleware/jwt.ts):

import jwt from "jsonwebtoken";

const jwt_secret_key = process.env.jwt_secret_key as string; // randomly generated. min length: 64 characters

export const protectedroute: requesthandler = async (req, _, next) => {
  const authheader = req.header("authorization");

  if (!authheader) {
    return next(notauthenticated());
  }

  const accesstoken = authheader.replace(new regexp("b[bb]earers"), "");

  try {
    const { userid } = validatejwt(accesstoken);
    const user = await userrepository.getuserbyid(parseint(userid));

    if (user) {
      req.user = user;
      next();
    } else {
      next(invalidaccesstoken());
    }
  } catch (err) {
    next(invalidaccesstoken());
  }
};

const validatejwt = (token: string, verifyoptions?: jwt.verifyoptions) => {
  const jwtverifyoptions = object.assign(
    { algorithms: "hs256" },
    verifyoptions,
    {
      issuer: "yourapi.com",
      audience: "yourapi.com:client",
    }
  );
  return jwt.verify(token, jwt_secret_key, jwtverifyoptions) as t;
};

使用中间件来保护路由:

import { protectedroute } from "@/middleware/jwt";

router.get("/user", protectedroute, async (req, res, next) => {
  const user = req.user!;
  res.json({ user });
});

创建身份验证控制器

现在,实现用于注册和登录的控制器:

注册控制器:

import argon from "argon2";

const signup = async (props) => {
  const { username, password, email } = props;

  await userrepo.getuser(username).then((res) => {
    if (res !== null) throw usernamenotavailable();
  });

  const hashedpass = await argon.hash(password, {
    timecost: 2,
    parallelism: 1,
    memorycost: 19456,
  });

  const newuser = await createuser({
    username,
    hashedpass,
    email,
  });

  const refreshtoken = await generaterefreshtoken(newuser.userid);
  const accesstoken = generateaccesstoken(newuser.userid);

  const { password: _, ...userres } = newuser;
  return { user: userres, accesstoken, refreshtoken };
};

登录控制器:

const login = async (props) => {
  const { username, password } = props;

  const user = await getuser(username).then((res) => {
    if (res === null) throw invalidlogincredentials();
    return res;
  });

  const isok = await argon.verify(user.password, password);

  if (isok) {
    const refreshtoken = await generaterefreshtoken(user.userid);
    const accesstoken = generateaccesstoken(user.userid);

    const { password: _, ...userres } = user;
    return { user: userres, accesstoken, refreshtoken };
  }

  throw invalidlogincredentials();
};

存储刷新令牌

刷新令牌提供长期身份验证。让我们创建一个数据库表来存储它们:

create table refresh_tokens (
    "id" serial primary key,
    "token" uuid not null default gen_random_uuid(),
    "token_family" uuid not null default gen_random_uuid(),
    "user_id" integer not null references users(id) on delete cascade,
    "active" boolean default true,
    "expires_at" timestamp not null,
    "created_at" timestamp not null default now()
);

代币生成器:

import jwt from "jsonwebtoken";

const jwt_secret_key = process.env.jwt_secret_key as string; // randomly generated. min length: 64 characters

const generateaccesstoken = (userid: number) => {
  const jwtsignoptions = object.assign(
    { algorithm: "hs256" },
    {},
    {
      issuer: "yourapi.com",
      audience: "yourapi.com:client",
    }
  );
  return jwt.sign({ userid: userid.tostring() }, jwt_secret_key, jwtsignoptions);
};

const generaterefreshtoken = async (userid: number, tokenfamily?: string) => {
  const expat = new date(new date().gettime() + 31 * 24 * 60 * 60 * 1000); // expire in 31 days
  const refreshtokenexp = expat.toisostring();

  const token = await createtokenquery({
    userid,
    tokenfamily,
    expiresat: refreshtokenexp,
  });

  return token;
};

刷新令牌逻辑:

实现逻辑来安全地处理刷新令牌:

const refreshToken = async ({ token }: RefreshTokenSchema) => {
  const tokenData = await getRefreshToken(token);

  if (!tokenData) throw forbiddenError();

  const { userId, tokenFamily, active } = tokenData;

  if (active) {
    // Token is valid and hasn't been used yet
    const newRefreshToken = await generateRefreshToken(userId, tokenFamily);
    const accessToken = generateAccessToken(userId);

    return { accessToken, refreshToken: newRefreshToken };
  } else {
    // Previously refreshed token used, invalidate all tokens in family
    await invalidateRefreshTokenFamily(tokenFamily);

    throw forbiddenError();
  }
};

在这篇 auth0 文章中了解有关刷新令牌和自动重用检测的更多信息。


结论

通过遵循本指南,您已经为 node.js api 构建了一个简单、安全的身份验证系统,并且依赖性最小。这种方法可确保您拥有完全控制权并遵守现代最佳安全实践。

如果您想节省时间和精力,请查看 vratix。我们的开源 cli 可以在几秒钟内建立一个功能齐全的 node.js 项目并进行身份验证。在 github 上探索我们完全实现的身份验证模块。


本指南对您有帮助吗?请在评论中告诉我们,或通过 x 与我们联系!

以上就是在 Nodejs 中进行身份验证的正确方法 [uide]的详细内容,更多请关注php中文网其它相关文章!