☰
LoopBack 4 认证装饰器 @authenticate 完全指南:元数据驱动的 API 端点保护
2026/10/14 17:31:24 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】loopback-next

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

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

导读

@authenticate是 LoopBack 4(@loopback/authentication包)中用于保护 REST API 端点的核心装饰器。本文以官方文档 Authentication-component-decorator.md 为主线,结合仓库源码(packages/authentication/src)与单元/验收测试,系统讲解该装饰器的四种语法形态、类级别与方法级别的使用方式、AuthenticationMetadata元数据的完整生命周期,以及如何结合策略注册与依赖注入获得当前用户。读完本文,你将能熟练使用@authenticate为控制器端点配置单策略、多策略、带选项与全局默认选项的认证,并理解其底层"只写元数据、不执行逻辑"的设计哲学。

一、装饰器在 LoopBack 4 中的定位:只写元数据,不做逻辑

LoopBack 4 中的装饰器与 TypeScript 标准装饰器并无二致——它们的作用是把元数据附加到类、方法、属性或参数上,本身不实现任何业务逻辑,只负责"记录配置"。

@authenticate正是这一哲学的代表:它把"该端点需要哪种认证策略、需要哪些策略选项"写入反射元数据,而真正执行认证的动作(AuthenticateAction/ 认证中间件)则通过依赖注入从上下文中读取这些元数据后再运行。因此:

  • 装饰器本身不校验任何凭据、不抛 401、不读写 Session;
  • 认证逻辑由注册的AuthenticationStrategy与AuthenticateFn完成;
  • 装饰器与执行层之间通过元数据绑定键解耦。

这一设计使得策略可以自由替换、多个策略可以叠加,而不必改动控制器代码。

二、@authenticate 的四种语法形态

@authenticate接受一个或多个参数,每个参数可以是策略名称字符串,也可以是包含strategy与options的对象。四种标准形态如下:

1. 单个策略(字符串)

@authenticate(strategyName)

2. 多个策略(字符串列表)

@authenticate(strategyName1, strategyName2)

3. 单个策略带选项

@authenticate({ strategy: strategyName, options: {option1: 'value1', option2: 'value2'} })

4. 多个策略各自带选项

@authenticate({ strategy: strategyName1, options: {option1: 'value1'} }, { strategy: strategyName2, options: {option2: 'value2'} })

其中:

  • strategyName是认证策略的唯一名称,必须与注册到应用中的策略实例的name属性完全一致(例如基本认证策略的name = 'basic')。
  • options一旦指定,其内容必须与该特定策略的约定相符——不同策略对options的解读各不相同,装饰器本身不校验其合法性,最终由策略在authenticate()中消费。

源码视角:参数如何被归一化为元数据

在 authenticate.decorator.ts 中,authenticate(...)函数把字符串参数统一包装成对象:

for (const strategy of strategies) { if (typeof strategy === 'object') { specs.push(strategy); } else { specs.push({strategy: strategy}); } }

也就是说@authenticate('basic')等价于@authenticate({strategy: 'basic'})。随后根据目标不同分派到不同的元数据键:

  • 作用在方法上时,写入AUTHENTICATION_METADATA_KEY(即authentication:method);
  • 作用在类上时,写入AUTHENTICATION_METADATA_CLASS_KEY(即authentication:class);
  • 若既不是方法也不是类(例如用在属性上),则抛出错误'@authenticate cannot be used on a property'。

对应元数据键定义在 keys.ts。AuthenticationMetadata的类型结构在 types.ts:

export interface AuthenticationMetadata { strategy: string; // 认证策略的唯一名称 options?: {[name: string]: any}; // 传给该策略的选项 skip?: boolean; // 跳过认证的标志 }

三、实战示例:为 /whoami 端点启用 basic 认证

以下示例取自官方文档,使用一个名为'basic'的自定义认证策略保护WhoAmIController中的/whoami端点(该策略的注册与实现详见 Authentication-component-strategy.md):

import {inject} from '@loopback/context'; import {AuthenticationBindings, authenticate} from '@loopback/authentication'; import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; import {get} from '@loopback/rest'; export class WhoAmIController { constructor( // `AuthenticationBindings.CURRENT_USER` 现在是 // `SecurityBindings.USER` 的别名(见 @loopback/security) @inject(SecurityBindings.USER) private userProfile: UserProfile, ) {} @authenticate('basic') @get('/whoami') whoAmI(): string { // `securityId` 是表示用户安全标识(通常是用户 id)的 Symbol, // 更多信息参见 docs/site/Security.md return this.userProfile[securityId]; } }

要点解读:

  • @authenticate('basic')只声明"该端点需要 basic 策略认证",并不真正执行认证;
  • 认证成功后,当前用户档案(UserProfile)被写入请求上下文,控制器通过@inject(SecurityBindings.USER)注入即可访问;
  • securityId是@loopback/security导出的 Symbol,用于从UserProfile中取出用户唯一标识(参见 packages/security/src/types.ts)。

补充:注入当前用户的两种推荐写法

官方文档特别提示:如果只有部分控制器方法标注了@authenticate,那么在构造函数中注入SecurityBindings.USER时必须写成可选注入,否则访问未认证端点时会因为找不到绑定而报错:

@inject(SecurityBindings.USER, {optional: true}) private userProfile: UserProfile,

或者,干脆不要在构造函数中注入,而是在真正标注了@authenticate的方法参数上进行方法级注入:

@authenticate('basic') @get('/whoami') whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile): string { return userProfile[securityId]; }

方法注入与构造注入的细节可分别参考 Dependency-injection.md 中的#method-injection、#constructor-injection与#optional-dependencies小节。

四、带选项的认证与全局默认选项

当需要给策略传选项时,使用对象形态:

@authenticate({strategy: 'basic', options: { /* 该策略的选项 */ }})

不过官方文档给出了一条实用建议:避免在大量端点上重复书写相同的 options。更优雅的做法是:

  1. 在一处定义策略的全局默认选项;
  2. 仅在需要覆盖的端点上,通过@authenticate传入覆盖选项;
  3. 由认证策略自身负责合并默认选项与覆盖选项。

完整实现步骤(绑定默认选项、定义AuthenticationStrategyOptions接口、在策略内用processOptions()合并覆盖)参见 Authentication-component-options.md。

源码视角:AuthMetadataProvider 的元数据解析与默认值回退

@authenticate的参数是通过AuthenticationBindings.METADATA绑定键、由AuthMetadataProvider提供出来的。在 auth-metadata.provider.ts 中可以看到完整的取值顺序:

value(): AuthenticationMetadata[] | undefined { if (!this.controllerClass || !this.methodName) return; const metadata = getAuthenticateMetadata(this.controllerClass, this.methodName); // 若 skip 为 true,则返回 undefined(跳过认证) if (metadata?.[0]?.skip) return undefined; if (metadata) return metadata; // 回退到组件级默认元数据 return this.options.defaultMetadata; }

其中getAuthenticateMetadata(定义于 authenticate.decorator.ts)遵循方法级优先、类级兜底的查找规则:

let metadata = MetadataInspector.getMethodMetadata(AUTHENTICATION_METADATA_METHOD_KEY, ...); if (metadata) return metadata; // 方法级注解优先 return MetadataInspector.getClassMetadata(AUTHENTICATION_METADATA_CLASS_KEY, ...);

这意味着:即使方法本身没有@authenticate,只要其所在类标注了@authenticate,方法同样继承类级认证配置;反之,方法级的@authenticate会覆盖类级配置。这一行为在单元测试 authenticate.decorator.unit.ts 中通过overrides class level metadata by method level用例得到验证。

默认元数据可通过配置组件注入,例如:

app .configure(AuthenticationBindings.COMPONENT) .to({defaultMetadata: [{strategy: 'xyz'}]});

这样,任何未标注@authenticate的方法也会默认走xyz策略(见 types.ts 中AuthenticationOptions.defaultMetadata的定义说明)。

五、类级别装饰与 @authenticate.skip() 跳过认证

除了方法级别,@authenticate还可以用在类上,为类内所有方法设置默认认证;个别无需认证的方法则用@authenticate.skip()显式豁免:

@authenticate('BasicStrategy') export class WhoAmIController { constructor(@inject(SecurityBindings.USER) private user: UserProfile) {} @get('/whoami') whoAmI(): string { return this.user[securityId]; } @authenticate.skip() @get('/hello') hello(): string { return 'Hello'; } }

上面代码中whoAmI虽无方法级装饰器,但会继承类级的BasicStrategy认证;而hello通过@authenticate.skip()放行匿名访问。

源码视角:skip 的实现

@authenticate.skip()是一个语法糖,实现于 authenticate.decorator.ts:

export const skip = () => authenticate({strategy: '', skip: true});

即生成一条{strategy: '', skip: true}的元数据。AuthMetadataProvider检测到metadata?.[0]?.skip为真时直接返回undefined,表示"该端点无需认证"。单元测试 authenticate.decorator.unit.ts 验证了方法级与类级 skip 两种场景。

六、多策略认证的执行规则

@authenticate支持同时传入多个策略:

@authenticate('BasicStrategy', 'JWTStrategy') @get('/whoami') whoAmI(): string { return this.user[securityId]; }

多个策略的执行规则与 Passport.js 的惯例一致(详见 Decorators_authenticate.md):

  • 策略按传入顺序依次执行;
  • 只要至少一个策略成功,请求即被放行,不抛错误;
  • 一旦某个策略成功或产生重定向,后续策略不再执行;
  • 若全部失败,抛出第一个策略的错误信息。

注意:不宜同时使用多个会触发重定向的策略(如 OAuth 登录重定向),因为第一次重定向就会中断执行链。

源码视角:认证动作的循环实现

上述规则在 auth-action.provider.ts 的AuthenticateActionProvider.action()中得到印证:

  • 通过getStrategies()获取策略数组(无元数据时返回undefined,直接放行匿名请求);
  • 依次调用strategy.authenticate(request),捕获异常存入authErrors;
  • 若返回RedirectRoute,则把重定向 URL 与状态码写入上下文后返回;
  • 若返回UserProfile,则通过setCurrentUser写入SecurityBindings.USER并返回该档案;
  • 全部失败时抛出USER_PROFILE_NOT_FOUND错误(若配置了failOnError: true,则某个策略一抛错就立即终止)。

另外,AuthenticationStrategyProvider(见 auth-strategy.provider.ts)负责按@authenticate中的名称在已注册的扩展点中查找策略,找不到时抛AUTHENTICATION_STRATEGY_NOT_FOUND错误。

七、认证成功之后:如何取回当前用户与认证元数据

1. 获取当前用户档案

请求通过认证后,当前用户的UserProfile会作为请求上下文的一个绑定(键为SecurityBindings.USER,即security.user)存在。控制器用依赖注入即可取得:

@inject(SecurityBindings.USER) private userProfile: UserProfile

兼容性说明:AuthenticationBindings.CURRENT_USER现已定义为SecurityBindings.USER的别名(见 keys.ts),新旧写法等价。

2. 获取 @authenticate 的原始参数(AuthenticationMetadata)

装饰器参数本身也可以通过依赖注入读取——使用AuthenticationBindings.METADATA绑定键:

@inject(AuthenticationBindings.METADATA) private metadata?: AuthenticationMetadata[],

该绑定由AuthMetadataProvider提供(AuthenticationComponent在 authentication.component.ts 中注册了AuthMetadataProvider、AuthenticateActionProvider、AuthenticationStrategyProvider与AuthenticationMiddlewareProvider四者)。返回类型为AuthenticationMetadata[]。

3. 元数据在认证链中的消费位置

AuthenticationStrategyProvider正是消费这份元数据的核心消费者:它注入AuthenticationBindings.METADATA,逐个取出strategy名称,再在已注册策略扩展点中查找同名策略并返回(见上文引用的 auth-strategy.provider.ts)。因此,装饰器→元数据→策略解析→策略执行的完整调用链为:

@authenticate('basic') ↓ 写入方法/类级元数据(authentication:method / authentication:class) AuthMetadataProvider(AuthenticationBindings.METADATA) ↓ 读取并返回 AuthenticationMetadata[] AuthenticationStrategyProvider ↓ 按名称查找策略实例 AuthenticateActionProvider.action() ↓ 依次执行 strategy.authenticate(request) 成功 → setCurrentUser(SecurityBindings.USER)

八、可验证的端到端行为(测试依据)

仓库中的验收测试 basic-auth-extension.acceptance.ts 覆盖了与本文装饰器相关的完整行为矩阵,可作为你实现时的回归参照:

场景期望结果
正确凭据访问/whoAmI200,返回用户 id
缺少Authorization头401Authorization header not found.
Header 前缀不是Basic401Authorization header is not of type 'Basic'.
Header 段数超过 2401 提示必须符合'Basic xxyyzz'格式
base64 解密后缺少:分隔401 提示必须包含:
未标注装饰器的方法匿名可访问(200)
使用未注册的策略名@authenticate('doesnotexist')401,code: 'AUTHENTICATION_STRATEGY_NOT_FOUND'
策略返回undefined(无用户档案)401,code: 'USER_PROFILE_NOT_FOUND'
注册的 basic 策略向 OpenAPI spec 注入securitySchemes.basic(http/basic)

其中"未标注装饰器的方法匿名可访问"与"未知策略报错"两条用例,直接验证了AuthMetadataProvider对元数据的解析、AuthenticationStrategyProvider按名查找失败抛错的路径,与本文第四、六节所述源码行为一一对应。

九、常见问题与最佳实践小结

  1. 装饰器不执行认证:记住@authenticate只是元数据声明,真正的认证发生在序列(Sequence)的认证动作或认证中间件中;如果请求直接绕过了序列/中间件,装饰器不会生效。
  2. 策略名必须精确匹配:装饰器中的strategyName要与策略实例的name属性完全一致,否则抛AUTHENTICATION_STRATEGY_NOT_FOUND。
  3. 部分端点认证时的注入写法:构造函数注入SecurityBindings.USER必须加{optional: true},或改为在受保护的方法参数上注入。
  4. 避免重复 options:把策略默认选项绑定到独立 binding,在需要覆盖的端点才传 options,让策略负责合并(见 Authentication-component-options.md)。
  5. 类级 vs 方法级:类级@authenticate是全类默认,方法级会覆盖类级;@authenticate.skip()可豁免单个方法或整个类。
  6. 多策略注意重定向:同一端点不要混用多个会重定向的策略。

延伸阅读

  • 装饰器参考:Decorators_authenticate.md
  • 认证策略的注册与实现:Authentication-component-strategy.md
  • 全局默认选项与覆盖:Authentication-component-options.md
  • 认证动作(Sequence 集成):Authentication-component-action.md
  • 组件总览:Loopback-component-authentication.md
  • 源码与测试:装饰器实现、类型定义、绑定键、装饰器单元测试、basic 认证验收测试
  • 后端
  • 微服务

【免费下载链接】loopback-next

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

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

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

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

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

立即咨询