深入解析基于 tRPC 的权限控制设计

举报
汪子熙 发表于 2025/08/01 19:22:19 2025/08/01
【摘要】 tRPC 是一种用于在类型安全环境中构建 API 的工具,基于 TypeScript。它与传统的 RESTful API 框架不同,因为它利用 TypeScript 的类型推断来减少前后端 API 的重复定义,达到了一个类型安全、一体化的编程方式。这使得开发者能够以较少的代码完成前后端通信,省去了 API 类型声明的一些繁琐步骤,并且在编译时就能发现错误,提升了开发效率和代码质量。为了理解此...

tRPC 是一种用于在类型安全环境中构建 API 的工具,基于 TypeScript。它与传统的 RESTful API 框架不同,因为它利用 TypeScript 的类型推断来减少前后端 API 的重复定义,达到了一个类型安全、一体化的编程方式。这使得开发者能够以较少的代码完成前后端通信,省去了 API 类型声明的一些繁琐步骤,并且在编译时就能发现错误,提升了开发效率和代码质量。

为了理解此代码,我们需要对 @trpc/server 和 Zod 这样的库有一定了解。@trpc/server 提供了一种用于构建类型安全后端 API 的方式,而 z 则是一个数据验证库,它确保了传入和传出数据符合设定的结构规范。tRPCError 是 tRPC 中用于处理错误的类型。

代码导入部分及基本类型定义

代码从 @trpc/serverzod 库中分别导入了 initTRPCTRPCError,并用 z 来进行后续数据验证。以下是这段代码的导入部分和类型定义的初步解析。

import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

上述导入的内容分别为:

  • initTRPC:用来初始化 tRPC,这样后续才能创建带有类型安全的 API 端点。
  • TRPCError:用来抛出 tRPC 中的标准错误,以便在不同的地方进行错误处理。
  • z:数据验证库,用于确保 API 请求的数据符合预期。

在导入部分之后,代码定义了一些基本类型,这些类型描述了我们在该代码中需要处理的业务实体和上下文。

type Organization = {
  id: string;
  name: string;
};
type Membership = {
  role: 'ADMIN' | 'MEMBER';
  Organization: Organization;
};
type User = {
  id: string;
  memberships: Membership[];
};
type Context = {
  /**
   * User is nullable
   */
  user: User | null;
};

这些类型在代码中起到了基础定义的作用:

  1. Organization 类型:包含 idname,代表一个组织实体,分别用字符串描述。
  2. Membership 类型:描述用户在组织中的角色,其中 role 字段限制为 ADMINMEMBER,同时通过 Organization 属性描述其所属于的组织。
  3. User 类型:包含一个 id 和一个 memberships 数组,memberships 数组中包含用户属于的多个组织及其角色。
  4. Context 类型:在 tRPC 中,Context 表示 API 端点执行时的上下文。这里的上下文包括一个可为空的 user 对象。

初始化 tRPC

接下来,代码调用了 initTRPC 来进行 tRPC 初始化。

const t = initTRPC.context<Context>().create();

通过调用 initTRPC.context<Context>().create(),我们做了以下几件事情:

  • 上下文类型的指定context<Context>() 确定了在此 tRPC 实例中,我们使用的上下文类型是之前定义的 Context,其中的 user 字段是可以为空的。
  • tRPC 的创建.create() 方法创建了 tRPC 实例,用于构建 API 端点和中间件。

这段代码生成了一个 t 对象,后续的操作都围绕着该对象进行。

公开的程序定义 publicProcedure

在 tRPC 中,procedure 是代表一个 API 端点的操作步骤,可以理解为一个函数的定义,通过它来完成某些特定的任务。

export const publicProcedure = t.procedure;

这里定义了一个 publicProcedure,它基于 t.procedure。这个过程没有任何权限限制,意味着任何用户(无论是否登录)都可以访问该程序。它定义了一种公开的接口,类似于 RESTful API 中的“公共端点”,用于暴露一些通用功能。

认证用户的程序 authedProcedure

export const authedProcedure = t.procedure.use(async function isAuthed(opts) {
  const { ctx } = opts;
  // `ctx.user` is nullable
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  return opts.next({
    ctx: {
      // ✅ user value is known to be non-null now
      user: ctx.user,
    },
  });
});

这个部分的代码相对更复杂,我们来分步骤进行拆解。

中间件的概念

中间件在 API 的调用过程中相当于一个过滤器,它可以在请求进入某个处理程序之前或之后做一些额外的操作。这里定义的 isAuthed 就是一个典型的中间件,用于对用户进行身份验证。

使用中间件定义 authedProcedure

t.procedure.use() 允许我们为过程附加一些中间件。这样,我们就可以在一个 API 请求到达处理程序之前对请求进行处理,例如,验证用户是否具有访问权限。

isAuthed 函数

isAuthed 函数被传递给 use(),它接收一个 opts 对象作为参数。这个 opts 对象包含了当前的上下文 ctx。对于 tRPC 来说,这个上下文是每次 API 调用都会提供的,所以我们可以在 ctx 中找到用户的相关信息。

代码中的逻辑如下:

  • 通过 const { ctx } = opts;,我们提取出了 opts 中的上下文对象 ctx
  • 接着,代码检查 ctx.user 是否为空。因为 Context 类型中 user 被定义为可能为空 (User | null),因此在进行下一步操作之前必须检查用户是否存在。
  • 如果 ctx.usernull,则抛出一个 TRPCError,错误代码为 'UNAUTHORIZED',这意味着用户未被授权访问此 API 端点。

错误处理机制

throw new TRPCError({ code: 'UNAUTHORIZED' });

此处代码通过 throw new TRPCError() 抛出一个 tRPC 特有的错误,并指定错误代码为 'UNAUTHORIZED'。这种错误机制在 tRPC 中用于明确标记不同类型的错误,从而方便客户端进行捕获和处理。在这里,错误代码 UNAUTHORIZED 用于告知用户没有适当的身份来访问该资源。

成功通过认证后的操作

如果用户通过认证,代码执行以下逻辑:

return opts.next({
  ctx: {
    // ✅ user value is known to be non-null now
    user: ctx.user,
  },
});

在这里,opts.next() 表示通过当前中间件的验证后继续往下执行下一个操作。

  • 通过给 ctx 传递一个更新后的对象(这里的 user 已经不再可能是 null),后续的处理过程可以安全地假定 user 不会为空,从而减少对 null 值的检查。

publicProcedureauthedProcedure 的应用场景

基于上述内容,我们可以看出,publicProcedureauthedProcedure 代表了两种不同类型的 API 端点:

  • publicProcedure:适用于无需用户身份认证的公开资源。例如,获取系统公告、注册等操作。
  • authedProcedure:用于需要用户身份认证的资源访问,例如用户个人资料、组织内的管理功能等。

下面我们来看一个具体的例子,以便更好理解这两者的区别和使用场景。

示例一:公开资源的获取

假设我们有一个公开的 API 端点用于获取系统的公告信息,可以这样定义:

export const getAnnouncements = publicProcedure.query(() => {
  // 公开的系统公告信息
  return [
    { id: '1', message: '系统将在明天进行维护' },
    { id: '2', message: '新功能上线,请查收' },
  ];
});

这里的 getAnnouncements 使用了 publicProcedure,任何人都可以访问这个接口以获取公告信息,不需要登录。

示例二:需要认证的用户资料获取

再来看一个用户个人资料的获取,假设我们只允许登录用户访问自己的资料:

export const getUserProfile = authedProcedure.query(({ ctx }) => {
  // 因为 authedProcedure 保证了 user 不会为空
  const user = ctx.user;

  return {
    id: user.id,
    memberships: user.memberships,
  };
});

在这个例子中,getUserProfile 使用了 authedProcedure,通过中间件确保只有登录用户可以访问自己的资料。在执行查询函数时,ctx.user 已经被保证为不为空,因此可以安全地访问用户的相关信息而无需额外的空值检查。

代码整体的逻辑分析

这段代码的核心目标在于建立一套权限控制体系,其中:

  • Context 用于描述请求上下文的结构,其中的 user 可以为空,用于表示当前请求的用户可能未登录。
  • t 是通过 initTRPC 初始化得到的 tRPC 实例,后续的所有程序定义、权限控制和业务逻辑都围绕它展开。
  • publicProcedureauthedProcedure 则分别用于定义无需登录和需要登录的 API 端点,这样可以在设计接口时保证不同端点的安全性。

中间件的设计与实现

中间件在现代 Web 编程中是一种常见的设计模式,用于在请求到达处理函数之前执行一些共享的逻辑,比如日志记录、身份验证、输入数据的规范化等。isAuthed 中间件通过 use() 方法附加到 authedProcedure 上,使得每次请求该过程时都会首先经过身份验证的逻辑,从而确保安全性。

在这里,我们通过 ctx.user 的空值检查来判断用户是否登录,并基于结果来决定是否继续请求处理。这样一种方式确保了代码的健壮性,同时也简化了后续的逻辑处理。每当访问需要权限的 API 时,开发人员无需再去考虑用户是否已认证,因为中间件已经帮助完成了这一部分的工作。

代码潜在的优化与扩展

这段代码虽然在现有的场景中已经实现了基本的认证机制,但我们还可以做一些扩展以应对更加复杂的需求。

扩展中间件进行角色验证

目前的中间件只验证了用户是否登录,如果我们还需要进一步验证用户在组织中的角色(如 ADMINMEMBER),可以扩展 isAuthed 中间件,检查用户的 memberships 中是否包含特定组织的管理员角色。下面的代码示例展示了如何做到这一点:

const isAdmin = t.middleware(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  const isAdmin = ctx.user.memberships.some(
    (membership) => membership.role === 'ADMIN'
  );

  if (!isAdmin) {
    throw new TRPCError({ code: 'FORBIDDEN' });
  }

  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

// 使用角色验证中间件定义新的过程
export const adminProcedure = t.procedure.use(isAdmin);

在这个例子中,isAdmin 中间件不仅验证了用户是否登录,还验证了用户是否具有管理员权限。如果没有管理员权限,则抛出 FORBIDDEN 错误。

细粒度的权限控制

在实际的开发中,不同的接口可能需要不同的权限。为了避免每次都重复编写身份验证逻辑,我们可以创建多个中间件,比如只需要登录验证的 isAuthed,需要管理员权限的 isAdmin,甚至还可以定义更细粒度的权限,例如只能查看自己组织内数据的验证逻辑。

结论

通过对这段代码的详细解析,我们可以看到它实现了一个基础的权限控制机制,以确保不同的 API 端点根据用户的身份和角色来决定是否可以被访问。代码中的每一个类型和函数设计都有其特定的用意,从用户类型的定义到上下文的构建,再到具体过程的权限控制,体现了较好的设计模式和代码复用性。

在这套体系中,公开的端点和受限的端点分别通过 publicProcedureauthedProcedure 来定义,通过 tRPC 的中间件机制,将身份认证逻辑进行了良好的封装,从而减少了代码重复,并增强了系统的安全性和可维护性。扩展这段代码,也可以在实现更复杂的权限管理、细化不同用户的访问权限等方面做进一步的优化。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。