- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
导读
@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。更优雅的做法是:
- 在一处定义策略的全局默认选项;
- 仅在需要覆盖的端点上,通过
@authenticate传入覆盖选项; - 由认证策略自身负责合并默认选项与覆盖选项。
完整实现步骤(绑定默认选项、定义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 覆盖了与本文装饰器相关的完整行为矩阵,可作为你实现时的回归参照:
| 场景 | 期望结果 |
|---|---|
正确凭据访问/whoAmI | 200,返回用户 id |
缺少Authorization头 | 401Authorization header not found. |
Header 前缀不是Basic | 401Authorization header is not of type 'Basic'. |
| Header 段数超过 2 | 401 提示必须符合'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按名查找失败抛错的路径,与本文第四、六节所述源码行为一一对应。
九、常见问题与最佳实践小结
- 装饰器不执行认证:记住
@authenticate只是元数据声明,真正的认证发生在序列(Sequence)的认证动作或认证中间件中;如果请求直接绕过了序列/中间件,装饰器不会生效。 - 策略名必须精确匹配:装饰器中的
strategyName要与策略实例的name属性完全一致,否则抛AUTHENTICATION_STRATEGY_NOT_FOUND。 - 部分端点认证时的注入写法:构造函数注入
SecurityBindings.USER必须加{optional: true},或改为在受保护的方法参数上注入。 - 避免重复 options:把策略默认选项绑定到独立 binding,在需要覆盖的端点才传 options,让策略负责合并(见 Authentication-component-options.md)。
- 类级 vs 方法级:类级
@authenticate是全类默认,方法级会覆盖类级;@authenticate.skip()可豁免单个方法或整个类。 - 多策略注意重定向:同一端点不要混用多个会重定向的策略。
延伸阅读
- 装饰器参考: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.
相关推荐
LoopBack 4 装饰器全景实践:OpenAPI 路由、@inject 依赖注入、@authenticate 认证与 Repository 模型装饰器
LoopBack 4 装饰器全景实践:OpenAPI 路由、@inject 依赖注入、@authenticate 认证与 Repository 模型装饰器 装饰
后端微服务LoopBack 4 认证动作(Authentication Action)实战:为自定义 Sequence 接入 authenticate 动作并保护 REST 端点
LoopBack 4 认证动作(Authentication Action)实战:为自定义 Sequence 接入 authenticate 动作并保护 RES
后端微服务TypeGraphQL装饰器元数据验证:确保元数据正确
TypeGraphQL装饰器元数据验证:确保元数据正确 在使用TypeGraphQL构建GraphQL API时,装饰器元数据验证是确保数据正确性的关键环节。本
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考