☰
LoopBack 4 认证动作(Authentication Action)实战:为自定义 Sequence 接入 authenticate 动作并保护 REST 端点
2026/10/13 6:19:31 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载

本文聚焦 LoopBack 4 认证体系中最核心的接入点——认证动作(Authentication Action):在基于Sequence(序列)的请求处理管线中,authenticate默认并不存在,需要开发者通过AuthenticateActionProvider提供的AuthenticateFn手动注入到自定义序列中。读完本文,你将掌握默认序列的执行步骤、AuthenticateActionProvider的底层机制(策略延迟解析、用户 Profile 写入上下文、协议无关错误码),并能在自己的应用中写出一个可运行、可绑定、可返回 401 的自定义认证序列,同时了解新版本中间件式序列下认证中间件的等价实现。

理解认证动作与 Sequence:authenticate 为什么默认缺席

在 LoopBack 4 的 REST 应用中,每一个请求都会经过一组无状态动作的编排,这组动作被称为Sequence(序列)。序列本质上是一系列步骤,用于控制RestServer如何响应传入请求。默认序列会注入并依次执行findRoute、parseParams、invoke、send、reject这五个动作。

在 Todo 示例的序列文件 中可以找到默认序列的经典实现(该示例当前版本已切换为MiddlewareSequence,但其对五个动作的语义描述仍然成立):

export class DefaultSequence implements SequenceHandler { /** * Constructor: Injects findRoute, invokeMethod & logError * methods as promises. * * @param {FindRoute} findRoute Finds the appropriate controller method, * spec and args for invocation (injected via SequenceActions.FIND_ROUTE). * @param {ParseParams} parseParams The parameter parsing function (injected * via SequenceActions.PARSE_PARAMS). * @param {InvokeMethod} invoke Invokes the method specified by the route * (injected via SequenceActions.INVOKE_METHOD). * @param {Send} send The action to merge the invoke result with the response * (injected via SequenceActions.SEND) * @param {Reject} reject The action to take if the invoke returns a rejected * promise result (injected via SequenceActions.REJECT). */ constructor( @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, @inject(SequenceActions.SEND) public send: Send, @inject(SequenceActions.REJECT) public reject: Reject, ) {} /** * Runs the default sequence. Given a handler context (request and response), * running the sequence will produce a response or an error. */ async handle(context: RequestContext): Promise<void> { try { const {request, response} = context; const route = this.findRoute(request); const args = await this.parseParams(request, route); const result = await this.invoke(route, args); debug('%s result -', route.describe(), result); this.send(response, result); } catch (error) { this.reject(context, error); } } }

每个动作的职责如下:

动作职责
findRoute找到合适的控制器方法、Swagger 规范与调用参数
parseParams解析 HTTP 请求,得到 API 参数列表
invoke调用 Application Controller 中定义的 API
send将 API 的返回结果写入 HTTP 响应
reject上述任一步骤出错时捕获错误并记录日志

关键点:默认情况下,authenticate不是动作序列的一部分。因此,如果你要为端点启用认证,必须修改默认序列,把认证动作加进去。

注意:如果你使用的是基于中间件的序列(middleware-based sequence),则不需要本节的操作——认证由自动发现并加入序列的中间件强制执行。详见本文「中间件式序列下的认证」一节与 REST 中间件序列文档。

AuthenticateActionProvider:认证动作的提供者与底层机制

认证动作AuthenticateFn由AuthenticateActionProvider类提供,该类的核心实现在 auth-action.provider.ts。原文档给出的机制说明代码如下:

// ------ CODE THAT EXPLAINS THE MECHANISM --------- export class AuthenticateActionProvider implements Provider<AuthenticateFn> { constructor( // The provider is instantiated for Sequence constructor, // at which time we don't have information about the current // route yet. This information is needed to determine // what auth strategy should be used. // To solve this, we are injecting a getter function that will // defer resolution of the strategy until authenticate() action // is executed. @inject.getter(AuthenticationBindings.STRATEGY) readonly getStrategy: Getter<AuthenticationStrategy>, @inject.setter(SecurityBindings.USER) readonly setCurrentUser: Setter<UserProfile>, ) {} /** * @returns authenticateFn */ value(): AuthenticateFn { return request => this.action(request); } /** * The implementation of authenticate() sequence action. * @param request The incoming request provided by the REST layer */ async action(request: Request): Promise<UserProfile | undefined> { const strategy = await this.getStrategy(); if (!strategy) { // The invoked operation does not require authentication. return undefined; } const userProfile = await strategy.authenticate(request); if (!userProfile) { // important to throw a non-protocol-specific error here let error = new Error( `User profile not returned from strategy's authenticate function`, ); Object.assign(error, { code: USER_PROFILE_NOT_FOUND, }); throw error; } this.setCurrentUser(userProfile); return userProfile; } }

机制拆解如下:

  • value()返回一个类型为AuthenticateFn的函数,该函数尝试获取认证策略(由AuthenticationStrategyProvider通过AuthenticationBindings.STRATEGY绑定解析得到)。
  • 如果该端点没有指定认证策略,认证动作立即返回undefined,请求继续往下走。
  • 如果该端点指定了认证策略,则调用策略的authenticate(request)方法。
  • 若返回了用户 Profile,说明认证成功,该 Profile 会通过SecurityBindings.USER绑定写入请求上下文;否则抛出USER_PROFILE_NOT_FOUND错误。

之所以在构造函数中注入的是@inject.getter(Getter 函数)而非直接注入策略实例,是因为 Provider 在序列构造时就被实例化,此时还不知道当前路由信息,而路由信息恰恰决定了应使用哪个策略。通过延迟解析,直到authenticate()动作真正执行时才解析策略。

现代源码的增强:多策略、failOnError 与重定向

当前仓库中的AuthenticateActionProvider(见 auth-action.provider.ts)在原始机制之上支持了更多能力,构造函数注入了策略 Getter、当前用户 Setter、重定向 URL/状态 Setter,以及组件配置AuthenticationOptions:

export class AuthenticateActionProvider implements Provider<AuthenticateFn> { constructor( @inject.getter(AuthenticationBindings.STRATEGY) readonly getStrategies: Getter< AuthenticationStrategy | AuthenticationStrategy[] | undefined >, @inject.setter(SecurityBindings.USER) readonly setCurrentUser: Setter<UserProfile>, @inject.setter(AuthenticationBindings.AUTHENTICATION_REDIRECT_URL) readonly setRedirectUrl: Setter<string>, @inject.setter(AuthenticationBindings.AUTHENTICATION_REDIRECT_STATUS) readonly setRedirectStatus: Setter<number>, @config({fromBinding: AuthenticationBindings.COMPONENT}) private readonly options: AuthenticationOptions = {}, ) {} value(): AuthenticateFn { return request => this.action(request); } async action(request: Request): Promise<UserProfile | undefined> { let strategies = await this.getStrategies(); if (strategies == null) { // The invoked operation does not require authentication. return undefined; } // convert to array if required strategies = Array.isArray(strategies) ? strategies : [strategies]; const authErrors: unknown[] = []; for (const strategy of strategies) { let authResponse: UserProfile | RedirectRoute | undefined = undefined; try { authResponse = await strategy.authenticate(request); } catch (err) { if (this.options.failOnError) { throw err; } authErrors.push(err); } // response from `strategy.authenticate()` could return an object of // type UserProfile or RedirectRoute if (RedirectRoute.isRedirectRoute(authResponse)) { const redirectOptions = authResponse; // bind redirection url and status to the context // controller should handle actual redirection this.setRedirectUrl(redirectOptions.targetLocation); this.setRedirectStatus(redirectOptions.statusCode); return; } else if (authResponse != null) { // if `strategy.authenticate()` returns an object of type UserProfile, // set it as current user const userProfile = authResponse as UserProfile; this.setCurrentUser(userProfile); return userProfile; } } if (authErrors.length) { throw authErrors[0]; } // important to throw a non-protocol-specific error here const error = new Error( `User profile not returned from strategy's authenticate function`, ); Object.assign(error, { code: USER_PROFILE_NOT_FOUND, }); throw error; } }

现代版本的行为要点:

  • 支持多个策略:一个端点可绑定多个策略,认证动作会按顺序逐个调用,只要有一个策略返回UserProfile即认证成功。
  • failOnError选项:当某个策略抛出异常时,默认行为是记录错误并继续尝试下一个策略;若组件配置failOnError: true(见 AuthenticationOptions 定义),则立即抛出错误终止认证。
  • 支持RedirectRoute:策略的authenticate()若返回RedirectRoute(常用于 OAuth 登录跳转),认证动作会将该重定向 URL 与状态码(通常是 302/303)绑定到上下文,交由控制器处理实际跳转。对应绑定键AUTHENTICATION_REDIRECT_URL与AUTHENTICATION_REDIRECT_STATUS定义在 keys.ts。
  • 所有策略都失败且存在错误时,抛出第一个错误;没有任何策略返回 Profile 时,抛出带code: USER_PROFILE_NOT_FOUND的协议无关错误。

相关类型与绑定键

  • AuthenticateFn类型定义在 types.ts:(request: Request) => Promise<UserProfile | undefined>。
  • AuthenticationStrategy接口(types.ts)要求实现name属性与authenticate(request)方法,返回值可以是UserProfile、RedirectRoute或undefined。
  • 绑定键AuthenticationBindings.AUTH_ACTION定义在 keys.ts,其值为'authentication.actions.authenticate',用于从上下文获取认证函数。
  • AuthenticationComponent(authentication.component.ts)会把AuthenticateActionProvider注册到AUTH_ACTION绑定键上,同时注册策略提供者与元数据提供者。

单元测试佐证

仓库中的单元测试 authentication.provider.unit.ts 验证了上述机制:

  • value()返回的函数能认证请求并返回用户(returns a function which authenticates a request and returns a user)。
  • 认证成功后更新当前用户(updates current user)。
  • 所有策略都失败时抛出错误;只要有一个策略成功则不抛错;failOnError: true时任一策略失败即抛错。
  • 未返回用户 Profile 时抛出code: 'USER_PROFILE_NOT_FOUND'的错误(对应测试throws USER_PROFILE_NOT_FOUND error when userprofile not returned)。

自定义序列接入认证动作:MyAuthenticatingSequence 实战

下面是一个改造后的自定义序列,它在invoke之前调用了authenticate动作。这是原文档给出的核心示例,也是仓库测试夹具 authentication.sequence.ts 中真实使用的模式:

export class MyAuthenticatingSequence implements SequenceHandler { constructor( // ... Other injections // ------ ADD SNIPPET --------- @inject(AuthenticationBindings.AUTH_ACTION) protected authenticateRequest: AuthenticateFn, // ------------- END OF SNIPPET ------------- ) {} async handle(context: RequestContext) { try { const {request, response} = context; const route = this.findRoute(request); // ------ ADD SNIPPET --------- //call authentication action await this.authenticateRequest(request); // ------------- END OF SNIPPET ------------- // Authentication successful, proceed to invoke controller const args = await this.parseParams(request, route); const result = await this.invoke(route, args); this.send(response, result); } catch (error) { // ------ ADD SNIPPET --------- if ( error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || error.code === USER_PROFILE_NOT_FOUND ) { Object.assign(error, {statusCode: 401 /* Unauthorized */}); } // ------------- END OF SNIPPET ------------- this.reject(context, error); return; } } }

注意到序列构造函数中新增的依赖注入:

@inject(AuthenticationBindings.AUTH_ACTION) protected authenticateRequest: AuthenticateFn,

绑定键AuthenticationBindings.AUTH_ACTION让我们拿到由AuthenticateActionProvider提供的、类型为AuthenticateFn的认证函数authenticateRequest。

使用要点:

  1. 位置:认证函数authenticateRequest可以在自定义序列中任何位于invoke动作之前的地方调用,从而保护端点。
  2. 错误码映射:AUTHENTICATION_STRATEGY_NOT_FOUND与USER_PROFILE_NOT_FOUND是两个协议无关(protocol-agnostic)的错误,必须在序列中捕获并赋予 HTTP 状态码401(Unauthorized)。具体返回什么 HTTP 错误码,由开发者在自己实现的认证策略或其自定义服务中决定。
  3. 失败即短路:认证过程中一旦抛出任何错误,端点的控制器函数永远不会被执行。
  4. 策略实现自身抛出的错误通常已带有statusCode;只有上述两个协议无关错误需要序列手动补上statusCode: 401。仓库测试夹具 authentication.sequence.ts 的注释明确说明了这一设计意图。

在 passport-login 示例 中可以看到一个更复杂的真实场景:该序列把authenticateRequest放在parseParams之后调用,因为登录场景需要先从路径参数中解析出 OAuth 提供者名称,再把oauth2-provider-name写入 query,最后才执行认证;认证失败且请求路径为/login时,则把UnauthorizedError事件交回给外层 Express 应用处理。

将认证序列绑定到应用

定义好执行认证动作的自定义序列后,必须把它绑定到应用(application.ts)上:

export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); //... // ------ ADD SNIPPET --------- this.sequence(MyAuthenticatingSequence); // ------------- END OF SNIPPET ------------- //... } }

一个完整的认证应用配置通常包含三部分(可参考 Authentication-component-options.md 的总结):加载认证组件、注册认证策略、绑定认证序列:

import { AuthenticationComponent, registerAuthenticationStrategy, } from '@loopback/authentication'; export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); //... // ------ ADD SNIPPET --------- // load the authentication component this.component(AuthenticationComponent); // register your custom authentication strategy registerAuthenticationStrategy(this, BasicAuthenticationStrategy); // use your custom authenticating sequence this.sequence(MyAuthenticatingSequence); // ------------- END OF SNIPPET ------------- } }

在 JWT 认证教程(对应示例 todo-jwt)中,你还可以看到完整的端到端流程:加载AuthenticationComponent与JWTAuthenticationComponent、在序列中启用 JWT 认证动作、创建登录/注册控制器、用@authenticate('jwt')保护 Todo 接口。

中间件式序列下的认证:AuthenticationMiddlewareProvider

原文档开头有一条重要提示:如果应用使用基于中间件的序列(middleware-based sequence),则无需手动添加认证动作,因为认证由自动发现并加入序列的中间件强制执行。

@loopback/rest自 6.0.0 起默认采用中间件式序列MiddlewareSequence(lb4 app生成的src/sequence.ts即继承它,参见 REST 中间件序列文档)。@loopback/authentication为此提供了AuthenticationMiddlewareProvider(与AuthenticateActionProvider同文件,见 auth-action.provider.ts):

@injectable( asMiddleware({ group: RestMiddlewareGroups.AUTHENTICATION, upstreamGroups: [RestMiddlewareGroups.FIND_ROUTE], }), ) export class AuthenticationMiddlewareProvider implements Provider<Middleware> { constructor( @inject(AuthenticationBindings.AUTH_ACTION) private authenticate: AuthenticateFn, ) {} value(): Middleware { return async (ctx, next) => { try { await this.authenticate(ctx.request); } catch (error) { if ( error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || error.code === USER_PROFILE_NOT_FOUND ) { error.statusCode = 401; } throw error; } return next(); }; } }

该中间件被注册到RestMiddlewareGroups.AUTHENTICATION组,并声明findRoute为其上游组。它内部复用同一个AUTH_ACTION绑定的AuthenticateFn,并在捕获两个协议无关错误时自动补上statusCode = 401——这正是自定义序列中那段错误映射代码的中间件版等价实现。

上图展示了中间件式序列中各组件的执行顺序:sendResponse最先进入(负责收集下游结果或捕获错误)、随后依次执行cors、apiSpec、middleware、findRoute、authentication、parseParams、invokeMethod。认证中间件在findRoute之后、parseParams之前执行,与自定义序列中"认证动作放在 invoke 之前"的位置语义一致。

因此,在新生成的应用中你通常什么都不用做:加载AuthenticationComponent后,认证中间件会被自动发现并加入序列。只有当你的应用仍使用传统动作式序列(例如需要精细控制每个动作的执行时机、或从旧版本迁移而来),才需要采用本文第三、四节的自定义序列方案。

错误码约定与协议无关设计

认证体系特意设计为"协议无关",即认证动作本身不直接与 HTTP 状态码耦合,而由外层(序列或中间件)负责映射。仓库中定义了两个错误码常量(types.ts):

  • AUTHENTICATION_STRATEGY_NOT_FOUND:策略解析失败时抛出。由 auth-strategy.provider.ts 在按名字查找不到策略时生成,错误消息形如The strategy 'doesnotexist' is not available.。
  • USER_PROFILE_NOT_FOUND:策略执行完成但没有返回用户 Profile 时抛出,错误消息为User profile not returned from strategy's authenticate function。

在自定义序列或认证中间件中,这两个错误应被映射为 HTTP 401。仓库的接受测试 basic-auth-extension.acceptance.ts 验证了完整行为:

  • 携带正确 Basic 凭据访问/whoAmI返回用户 ID;
  • 缺少Authorization头时返回statusCode: 401的UnauthorizedError;
  • 使用不存在的策略名@authenticate('doesnotexist')时返回code: 'AUTHENTICATION_STRATEGY_NOT_FOUND'、statusCode: 401;
  • 策略返回undefined时返回code: 'USER_PROFILE_NOT_FOUND'、statusCode: 401;
  • 未加@authenticate装饰器的方法允许匿名访问(认证动作因没有策略而直接返回)。

延伸阅读

  • 认证装饰器(Authentication Decorator):@authenticate的语法与元数据,即"端点如何声明需要认证"。
  • 认证策略(Authentication Strategy):自定义策略的实现与注册。
  • 管理自定义认证策略选项:defaultMetadata、failOnError等组件配置。
  • REST 中间件序列:中间件式序列的组排序与扩展方式。
  • JWT 认证教程:完整可运行的 JWT 认证应用。
  • 认证装饰器参考:@authenticate装饰器的详细 API。
  • 后端
  • 微服务

【免费下载链接】loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载
上一篇:Spring AI流式模式下Token计算的3种高效解决方案
下一篇:百度网盘提取码效率工具:快速获取分享资源的实用方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询