机制完全指南:从方法鉴权到基于角色的访问控制)
后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载守卫Guard是 Midway 从 v3.6.0 开始提供的一套请求鉴权能力它在全局中间件执行之后、路由业务方法执行之前根据权限、角色、访问控制列表ACL等运行时条件决定请求是否可以被路由处理程序处理。本篇指南以midwayjs/koa为例完整讲解守卫的编写、在路由/方法/全局三个层面的挂载方式、自定义错误抛出以及守卫与中间件的定位差异并基于仓库源码剖析其底层执行链路最后给出一个可直接落地的基于角色的访问控制示例。读完本文你将能够在自己项目中独立实现一套与路由方法深度绑定的鉴权体系。为什么需要守卫从中间件的局限说起在引入守卫之前Midway 应用中的登录、权限校验等逻辑普遍放在中间件里。中间件确实可以拦截请求但它有两个明显的局限逻辑过于通用中间件无法优雅地和具体的路由方法结合针对“这个接口只允许 admin 角色访问另一个接口允许 user 角色访问”这类细粒度场景中间件写起来会非常笨拙无法精确定位路由中间件虽然能拿到路由信息但无法明确得知请求最终会进入哪个具体的路由控制器方法除非额外做一次路由匹配做路由级权限控制时性能与语义都不理想。为此Midway 在中间件之后、路由方法之前设计了守卫。守卫已经进入了路由方法内部可以直接拿到supplierClz目标类和methodName目标方法名天然适合做基于路由的方法鉴权。下面我们以midwayjs/koa为例展开讲解。编写第一个守卫目录约定一般情况下守卫统一放在src/guard目录下。创建一个src/guard/auth.guard.ts用于验证路由是否能被用户访问➜ my_midway_app tree . ├── src │ ├── controller │ │ ├── user.controller.ts │ │ └── home.controller.ts │ ├── interface.ts │ ├── guard │ │ └── auth.guard.ts │ └── service │ └── user.service.ts ├── test ├── package.json └── tsconfig.json守卫的最小实现Midway 使用Guard装饰器标识一个类为守卫代码如下import { IMiddleware, Guard, IGuard } from midwayjs/core; import { Context } from midwayjs/koa; Guard() export class AuthGuard implements IGuardContext { async canActivate(context: Context, supplierClz, methodName: string): Promiseboolean { // ... } }canActivate是守卫的核心方法用于在请求中验证是否可以访问后续的方法返回true时后续的方法会被执行返回false时框架会抛出 403 错误ForbiddenError。从源码看IGuard接口定义在 packages/core/src/interface.tsexport interface IGuardCTX unknown { canActivate( ctx: CTX, supplierClz: new (...args) any, methodName: string ): boolean | Promiseboolean; }接口约定三个参数的含义如下参数类型说明context/ctxCTX当前请求上下文在 Koa 下即Context可从中读取ctx.user、ctx.headers、ctx.state等请求信息supplierClznew (...args) any被守卫保护的目标类Controller 或 Service用于读取类级元数据methodNamestring被守卫保护的目标方法名用于读取方法级元数据并做方法级判断Guard装饰器背后的实现Guard并非一个空壳装饰器它的实现在 packages/core/src/decorator/common/guard.ts本质上做了两件事将类标记为可被 IoC 容器托管的Provide()组件并将其作用域设为Singleton单例export function Guard(): ClassDecorator { return target { Provide()(target); Scope(ScopeEnum.Singleton)(target); }; }这意味着守卫实例由 Midway 容器按单例模式创建和缓存多个请求共享同一个守卫实例鉴权逻辑中不应保存请求级可变状态。同时也解释了为什么守卫类本身可以注入其他依赖如Inject服务——它首先是一个标准的 IoC 组件。使用守卫路由守卫与全局守卫守卫可以被应用到不同框架上在 HTTP 框架下可以应用到全局、Controller 类和方法三个层面在其他 Framework 实现中仅能在方法上使用。路由守卫UseGuard装饰器编写完守卫后需要把它应用到控制器路由上。UseGuard装饰器可以同时作用于类和方法。应用到类上保护整个 Controller 的所有方法import { Controller } from midwayjs/core; import { AuthGuard } from ../guard/auth.guard; UseGuard(AuthGuard) Controller(/) export class HomeController { }应用到方法上只保护特定路由方法import { Controller, Get } from midwayjs/core; import { ReportMiddleware } from ../middleware/report.middlweare; import { AuthGuard } from ../guard/auth.guard; Controller(/) export class HomeController { UseGuard(AuthGuard) Get(/, { middleware: [ ReportMiddleware ]}) async home() { } }也可以传入守卫数组按顺序依次执行UseGuard([AuthGuard, Auth2Guard])从实现上看UseGuard在 packages/core/src/decorator/common/guard.ts 中会把单个守卫统一规整为数组并写入元数据GUARD_KEYexport function UseGuard( guardOrArr: CommonGuardUnion ): ClassDecorator MethodDecorator { return ( target: any, propertyKey?: string, descriptor?: PropertyDescriptor ) { if (!Array.isArray(guardOrArr)) { guardOrArr [guardOrArr]; } MetadataManager.defineMetadata(GUARD_KEY, guardOrArr, target, propertyKey); }; }注意UseGuard不传propertyKey时写的是类级元数据传了propertyKey时写的是方法级元数据这正是“类守卫”和“方法守卫”能够在后续执行链中被区分开来的基础。全局守卫useGuard方法全局守卫需要在应用启动前注册进当前框架的守卫列表。在src/configuration.ts中通过onReady生命周期调用useGuard// src/configuration.ts import { App, Configuration } from midwayjs/core; import * as koa from midwayjs/koa; import { AuthGuard } from ./guard/auth.guard; Configuration({ imports: [koa] // ... }) export class MainConfiguration { App() app: koa.Application; async onReady() { this.app.useGuard(AuthGuard); } }同理可以添加多个守卫async onReady() { this.app.useGuard([AuthGuard, Auth2Guard]); }从源码看useGuard定义在 packages/core/src/baseFramework.ts它把守卫交给guardManager的addGlobalGuard方法统一管理public useGuard(guards: CommonGuardUnionCTX) { return this.guardManager.addGlobalGuard(guards); }而addGlobalGuard在 packages/core/src/common/guardManager.ts 中同样兼容单值和数组两种传参方式public addGlobalGuard(guards: CommonGuardUnionCTX) { if (!Array.isArray(guards)) { this.push(guards); } else { this.push(...guards); } }守卫的完整执行顺序GuardManager.runGuard是守卫机制的调度核心实现在 packages/core/src/common/guardManager.ts其执行顺序严格为全局守卫遍历this全局守卫列表中注册的所有守卫类守卫读取目标类上GUARD_KEY元数据逐个执行方法守卫读取目标方法上GUARD_KEY元数据逐个执行。// 核心逻辑节选 // check global guard for (const Guard of this) { ... if (!isPassed) return false; } // check class Guard const classGuardList MetadataManager.getOwnMetadata(GUARD_KEY, supplierClz); // check method Guard const methodGuardList MetadataManager.getOwnMetadata(GUARD_KEY, supplierClz, methodName);任一守卫canActivate返回falserunGuard立即短路返回false后续守卫不再执行。每个守卫实例都是通过ctx.requestContext.getAsync(Guard)从请求级容器中获取的因此守卫中注入的组件遵循请求作用域规则。对于 HTTP 路由调用链在 packages/core/src/common/webGenerator.ts 中表现为控制器被包装成 Koa 中间件先执行runGuard未通过时抛出 403public generateKoaController(routeInfo: RouterInfo) { return async (ctx, next) { if (routeInfo.controllerClz typeof routeInfo.method string) { const isPassed await this.app .getFramework() .runGuard(ctx, routeInfo.controllerClz, routeInfo.method); if (!isPassed) { throw new httpError.ForbiddenError(); } } // ... 执行目标方法 }; }需要说明的是runGuard这一统一入口不止服务于 Koa从仓库源码看web-express、faas、socketio、ws、grpc、bull、rabbitmq 等多个框架都在各自的关键调用点接入了runGuard这也是文档中提到“其他 Framework 实现中仅能在方法上使用守卫”的源码依据。自定义错误默认情况下守卫的canActivate返回false时框架会抛出 403 错误ForbiddenError。这个错误类定义在 packages/core/src/error/http.ts内部携带 HTTP 状态码 403/** * 403 http error, Means that the request is legal, but the server is rejecting to answer it. */ export class ForbiddenError extends MidwayHttpError { constructor(resOrMessage?: ResOrMessage) { super(resOrMessage, HttpStatus.FORBIDDEN); } }你也可以在守卫中自行决定需要抛出的错误比如针对特定方法抛出不同的错误类型import { IMiddleware, Guard, IGuard, httpError } from midwayjs/core; import { Context } from midwayjs/koa; Guard() export class AuthGuard implements IGuardContext { async canActivate(context: Context, supplierClz, methodName: string): Promiseboolean { // ... if (methodName xxx) { throw new httpError.ForbiddenError(); } return true; } }:::tip 注意全局错误处理器Filter也会拦截守卫抛出的错误因此自定义错误同样会走统一的错误处理流程包括日志记录、响应格式封装等。 :::守卫和中间件的区别维度中间件Middleware守卫Guard执行时机在守卫之前执行在全局中间件之后、路由业务方法之前执行定位通用的横切逻辑登录、用户识别、安全校验等基于路由的权限控制路由信息有路由信息但无法明确得知具体进入的是哪个实际路由控制器除非额外查询匹配已进入路由方法内部直接拿到目标类与目标方法性能需要额外匹配才能定位路由省去路由匹配性能上有较大优势一句话总结中间件负责“这个请求谁来都能做的通用处理”守卫负责“这个请求到了具体方法门口该不该放行”。两者可以组合使用——例如先用中间件完成用户身份识别并把角色写入ctx.user再用守卫基于角色做路由级放行这正是下一节示例的套路。实战基于角色的鉴权示例RBAC一般情况下我们会把方法访问和角色关联起来。下面完整实现一个基于用户角色的访问控制。第一步定义Role装饰器创建一个src/decorator/role.decorator.ts用savePropertyMetadata把角色元数据保存到方法上// src/decorator/role.decorator.ts import { savePropertyMetadata } from midwayjs/core; export const ROLE_META_KEY role:name export function Role(roleName: string | string[]): MethodDecorator { return (target, propertyKey, descriptor) { roleName [].concat(roleName); // 只保存元数据 savePropertyMetadata(ROLE_META_KEY, roleName, target, propertyKey); }; }这里savePropertyMetadata是 Midway 提供的元数据工具与守卫侧读取时使用的getPropertyMetadata一一对应构成“写元数据 → 读元数据”的完整闭环。第二步编写角色鉴权守卫在src/guard/auth.guard.ts中通过getPropertyMetadata读取方法上注册的角色信息并与当前用户角色比对// src/guard/auth.guard.ts import { Guard, IGuard, getPropertyMetadata } from midwayjs/core; import { Context } from midwayjs/koa; import { ROLE_META_KEY } from ../decorator/role.decorator; Guard() export class AuthGuard implements IGuardContext { async canActivate(context: Context, supplierClz, methodName: string): Promiseboolean { // 从类元数据上获取角色信息 const roleNameList getPropertyMetadatastring[](ROLE_META_KEY, supplierClz, methodName); if (roleNameList roleNameList.length context.user.role) { // 假设中间件已经拿到了用户角色信息保存到了 context.user.role 中 // 直接判断是否包含该角色 return roleNameList.includes(context.user.role); } return false; } }关键点说明getPropertyMetadatastring[](ROLE_META_KEY, supplierClz, methodName)读取的是方法级角色元数据context.user.role依赖前置的登录/用户识别中间件预先填充守卫本身只做“角色比对”这一件事若方法上没有声明任何角色或用户没有角色守卫保守地返回false拒绝访问。第三步在路由上使用该守卫将AuthGuard挂到 Controller 类上作为统一防线再通过Role([admin])精确声明哪个方法只允许 adminimport { Controller, Get, UseGuard } from midwayjs/core; import { ReportMiddleware } from ../middleware/report.middlweare; import { AuthGuard } from ../guard/auth.guard; import { Role } from ../decorator/role.decorator; UseGuard(AuthGuard) Controller(/user) export class HomeController { // 只允许 admin 访问 Role([admin]) Get(/getUserRoles) async getUserRoles() { // ... } }最终效果只有当ctx.user.role返回了admin的时候才会被允许访问/getUserRoles路由否则守卫返回false框架自动抛出 403 错误。参考仓库中的守卫测试用例如果你希望进一步验证守卫机制的行为仓库在 packages/core/test/fixtures/base-app-with-guard/src/configuration.ts 提供了一个完整的守卫测试示例它同时覆盖了三种挂载方式MainGuard、Main2Guard通过this.app.useGuard(...)注册为全局守卫ClzGuard通过UseGuard(ClzGuard)挂在UserService类上MethodGuard通过UseGuard(MethodGuard)挂在invoke2方法上且其canActivate返回false用于验证拒绝路径。Guard() export class MethodGuard implements IGuardany { async canActivate(ctx: any): Promiseboolean { return false; } } Provide() UseGuard(ClzGuard) export class UserService { async invoke() { return hello invoke; } UseGuard(MethodGuard) async invoke2() { return hello invoke2; } }之后在onReady中手动调用this.app.getFramework().runGuard(ctx, UserService, invoke)和runGuard(ctx, UserService, invoke2)对比结果即可直观看到全局守卫、类守卫、方法守卫叠加时的执行效果与短路行为。核心执行器GuardManager的完整实现位于 packages/core/src/common/guardManager.ts是理解整个守卫机制的最佳入口。小结Midway 的守卫机制为路由级鉴权提供了一个比中间件更精准、性能更优的落点用Guard()声明守卫类实现IGuardContext.canActivate返回true放行、返回false抛出 403用UseGuard挂载到 Controller 类或方法上用app.useGuard注册全局守卫三层守卫按“全局 → 类 → 方法”顺序短路执行守卫内可以读取类与方法元数据、注入容器组件、主动抛出任意 HTTP 错误并配合savePropertyMetadata/getPropertyMetadata实现装饰器驱动的声明式权限如Role([admin])守卫在全局中间件之后、路由方法之前执行与中间件分工明确、可组合使用。这套能力从 v3.6.0 起可用并且已在 Koa、Express、Faas、Socket.io、gRPC、Bull、RabbitMQ 等多个框架的调用链中接入是构建可维护、可审计的权限体系的核心基础设施。赞分享后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载相关推荐RedwoodJS 基于角色的访问控制RBAC完全指南从身份认证到 Web/Api 双侧鉴权RedwoodJS 基于角色的访问控制RBAC完全指南从身份认证到 Web/Api 双侧鉴权 本文基于 RedwoodJS 仓库中 docs/versio后端前端Web框架开发工具如何在Mac上部署Unlimited-OCR-bf165分钟快速上手AI文档解析工具如何在Mac上部署Unlimited OCR bf165分钟快速上手AI文档解析工具 Unlimited OCR bf16是百度官方Unlimited OCR后端前端Web框架开发工具零信任时代的Kibana权限守卫基于角色的访问控制实战指南零信任时代的Kibana权限守卫基于角色的访问控制实战指南 在数据安全与日俱增的今天您是否还在为Kibana中复杂的权限管理焦头烂额是否担心敏感日志数据被前端数据可视化数据分析后端可观测性上一篇OpenCore Simplify 完整教程从硬件报告到黑苹果 OpenCore EFI 自动生成下一篇如何实现iBATIS到MyBatis的无缝迁移企业级框架升级的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考