☰
LoopBack 4 认证组件 `@loopback/authentication` 完全指南:从装饰器到策略的端到端实践
2026/10/14 2:17:27 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】loopback-next

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

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

导读

本文以@loopback/authentication包为主线,系统讲解 LoopBack 4 认证组件的完整架构与落地方法:从用@authenticate装饰器声明端点认证需求,到在自定义 Sequence 中插入认证 Action,再到编写并注册可复用的认证策略,最后深入策略选项的管理机制。读完本文,你将能独立为 LoopBack 4 应用接入任意自定义认证策略(如 Basic、JWT 或基于 Passport 的策略),并理解其底层依赖注入与扩展点的工作方式。

一、认证组件总体架构

@loopback/authentication是 LoopBack 4 中负责 API 端点安全认证的核心组件。从高层次看,该组件由以下四类构件组成:

  • 一个装饰器(@authenticate),用于在控制器方法(或类)上表达认证需求;
  • 一个Provider,用于读取方法级别的认证元数据(AuthenticationMetadata);
  • 一个认证 Action,作为 REST Sequence 中的一个环节来强制执行认证;
  • 一个扩展点(extension point),用于发现所有已注册的认证策略并完成策略的委派调度。

其高层架构如下:

将视角拉近,认证组件与控制器、Sequence、策略、用户服务之间的协作关系如下图所示:

概括地说,要让 API 端点受到保护,需要完成以下四件事:

  • 应用开发者:为控制器端点加上@authenticate(strategyName, options?)装饰器;
  • 应用开发者:在自定义 Sequence 中插入认证 Action(若使用默认的中间件式 Sequence 则无需手动插入);
  • 扩展开发者:编写一个带有唯一name的认证策略类;
  • 应用开发者:将自定义认证策略注册(bind)到应用。

其余工作全部由认证组件接管。

二、安装

在应用目录下安装认证组件包:

npm install --save @loopback/authentication

该包对外导出的内容(见 packages/authentication/src/index.ts)包括认证组件本体、装饰器、绑定键(AuthenticationBindings)、三个 Provider、认证相关服务接口(如TokenService、UserService、UserIdentityService)以及全部类型定义。

三、挂载 AuthenticationComponent

要使用认证能力,首先必须在应用的application.ts中加载AuthenticationComponent:

import {AuthenticationComponent} from '@loopback/authentication'; //... export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); //... // 挂载认证系统 this.component(AuthenticationComponent); //... } }

AuthenticationComponent的真实实现(见 packages/authentication/src/authentication.component.ts)如下:

@injectable({tags: {[ContextTags.KEY]: AuthenticationBindings.COMPONENT}}) export class AuthenticationComponent implements Component { providers = { [AuthenticationBindings.AUTH_ACTION.key]: AuthenticateActionProvider, [AuthenticationBindings.STRATEGY.key]: AuthenticationStrategyProvider, [AuthenticationBindings.METADATA.key]: AuthMetadataProvider, [AuthenticationBindings.AUTHENTICATION_MIDDLEWARE.key]: AuthenticationMiddlewareProvider, }; }

可以看到,组件主体就是一组 Provider 的集合,它们分别承担不同的职责:

绑定键Provider返回内容
AuthenticationBindings.METADATA.keyAuthMetadataProviderAuthenticationMetadata类型的认证装饰器元数据
AuthenticationBindings.AUTH_ACTION.keyAuthenticateActionProviderAuthenticateFn类型的认证函数
AuthenticationBindings.STRATEGY.keyAuthenticationStrategyProviderAuthenticationStrategy类型的认证策略
AuthenticationBindings.AUTHENTICATION_MIDDLEWARE.keyAuthenticationMiddlewareProvider自动挂载到中间件式 Sequence 的认证中间件

其中前三个 Provider 正是原文档描述的核心;第四个 Provider(AUTHENTICATION_MIDDLEWARE)是认证组件随中间件式 Sequence 演进后新增的成员——在使用中间件式 Sequence(新生成应用的默认形态)时,认证逻辑由中间件自动发现并执行,无需手动改 Sequence。各 Provider 的用途与返回值将在下文对应小节逐一展开。

所有绑定键均定义在 packages/authentication/src/keys.ts 中,值得关注的还有:AuthenticationBindings.CURRENT_USER是SecurityBindings.USER(来自@loopback/security)的向后兼容别名,因此在控制器中注入当前用户统一使用SecurityBindings.USER即可。

四、用@authenticate装饰器声明认证需求

LoopBack 4 的装饰器与标准 TypeScript 装饰器无异:它们只在类、方法、属性或参数上附加元数据,本身不添加任何功能逻辑。@authenticate正是这样——它把"该端点需要认证、用哪个策略认证"的声明写入元数据。

4.1 装饰器语法

  • 单一策略:@authenticate(strategyName)

  • 多策略:@authenticate(strategyName1, strategyName2)

  • 带选项的单一策略:

    @authenticate({ strategy: strategyName, options: {option1: 'value1', option2: 'value2'} })
  • 带选项的多策略:

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

其中strategyName是认证策略的唯一名称;options对象出现时必须与该策略相关。

4.2 方法级用法示例

下面为控制器WhoAmIController的端点/whoami应用名为'basic'的自定义策略(该策略的注册方法见第六节):

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` 的别名 @inject(SecurityBindings.USER) private userProfile: UserProfile, ) {} @authenticate('basic') @get('/whoami') whoAmI(): string { // `securityId` 是表示用户安全 ID(通常是用户 ID)的 Symbol return this.userProfile[securityId]; } }

请求认证成功后,当前用户档案(UserProfile)会挂在请求上下文上,通过SecurityBindings.USER绑定键依赖注入即可取得。

注意:如果控制器中只有部分方法被@authenticate装饰,那么构造函数中对SecurityBindings.USER的注入必须写成@inject(SecurityBindings.USER, {optional: true}),以免访问未认证端点时出现绑定错误;更推荐的做法是把SecurityBindings.USER的注入放在真正被@authenticate装饰的方法参数上而非构造函数中。详见 Method Injection、Constructor Injection 与 Optional Dependencies。

4.3 类级用法与@authenticate.skip()

@authenticate同样可以放在类上,为该类所有方法设定默认认证;方法级装饰器的优先级高于类级。若要放行某个方法,可用@authenticate.skip()(源码中由 authenticate.decorator.ts 提供的语法糖实现,实际写入{strategy: '', skip: true}元数据):

@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'; } }

4.4 多策略的执行语义

@authenticate可接受多个策略,其执行逻辑与 Passport.js 类似(详见 Decorators_authenticate):

  • 策略按声明顺序依次执行;
  • 只要有一个策略成功,请求继续处理,不再报错;
  • 一旦某个策略成功或发起重定向(RedirectRoute),后续策略不再执行;
  • 若全部策略失败,抛出第一个策略的错误信息。

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

4.5 元数据从何而来

@authenticate的实现在 packages/authentication/src/decorators/authenticate.decorator.ts 中:方法级元数据存储于AUTHENTICATION_METADATA_METHOD_KEY('authentication:method'),类级元数据存储于AUTHENTICATION_METADATA_CLASS_KEY('authentication:class'),两者均定义在 keys.ts 中。读取时getAuthenticateMetadata先查方法级,再回退到类级。

装饰器参数可通过AuthenticationBindings.METADATA绑定键以依赖注入方式取得,返回AuthenticationMetadata数组(由AuthMetadataProvider提供)。第六节将看到,AuthenticationStrategyProvider正是利用这份元数据来解析某个端点装饰器里指定的策略name。

五、认证 Action:把认证织入请求处理链路

在 LoopBack 4 中,每个 REST 请求都会流经一组无状态的 Action 集合,即 Sequence。

注意:如果使用基于中间件的 middleware-based sequence(新生成应用的默认形态),本节内容无需手动操作——认证由自动发现并加入 Sequence 的中间件强制执行(对应组件中的AuthenticationMiddlewareProvider)。本节针对使用传统自定义 Sequence 的场景。

5.1 默认 Sequence 里没有认证环节

默认 Sequence 依次执行findRoute、parseParams、invoke、send、reject五个动作。以 Todo 示例的 sequence 为代表的默认实现代码如下:

export class DefaultSequence implements SequenceHandler { 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, ) {} 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); this.send(response, result); } catch (error) { this.reject(context, error); } } }

authenticate默认不在这一序列中,因此需要改造 Sequence 加入认证 Action。

5.2AuthenticateActionProvider的实现机制

认证 ActionAuthenticateFn由AuthenticateActionProvider提供,其源码位于 packages/authentication/src/providers/auth-action.provider.ts:

export class AuthenticateActionProvider implements Provider<AuthenticateFn> { constructor( // Provider 在 Sequence 构造函数实例化时被创建,此时还没有当前路由信息, // 而路由信息是决定采用哪个策略的前提。 // 因此这里注入 getter 函数,把策略解析延迟到 authenticate() 真正执行时。 @inject.getter(AuthenticationBindings.STRATEGY) readonly getStrategy: Getter<AuthenticationStrategy>, @inject.setter(SecurityBindings.USER) readonly setCurrentUser: Setter<UserProfile>, ) {} value(): AuthenticateFn { return request => this.action(request); } async action(request: Request): Promise<UserProfile | undefined> { const strategy = await this.getStrategy(); if (!strategy) { // 该操作不需要认证 return undefined; } const userProfile = await strategy.authenticate(request); if (!userProfile) { // 抛出与协议无关的错误 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绑定解析)。若端点未指定任何策略,Action 立即返回;若指定了策略,则调用其authenticate(request)。返回了 UserProfile 即代表认证成功,并将用户档案写入请求上下文(SecurityBindings.USER绑定);否则抛出错误。

5.3 编写带认证的 Sequence

在自定义 Sequence 中,先通过构造器注入认证 Action,再在invoke之前调用它:

export class MyAuthenticatingSequence implements SequenceHandler { constructor( // ... 其他注入 @inject(AuthenticationBindings.AUTH_ACTION) protected authenticateRequest: AuthenticateFn, ) {} async handle(context: RequestContext) { try { const {request, response} = context; const route = this.findRoute(request); // 调用认证 Action await this.authenticateRequest(request); // 认证成功,继续调用控制器 const args = await this.parseParams(request, route); const result = await this.invoke(route, args); this.send(response, result); } catch (error) { if ( error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || error.code === USER_PROFILE_NOT_FOUND ) { Object.assign(error, {statusCode: 401 /* Unauthorized */}); } this.reject(context, error); return; } } }

其中AUTHENTICATION_STRATEGY_NOT_FOUND与USER_PROFILE_NOT_FOUND是两个与协议无关的错误码(定义于 packages/authentication/src/types.ts),必须在 Sequence 中捕获并映射为 HTTP 401。需要注意的是:是否抛出适当的 HTTP 错误码,属于自定义策略及其服务自身的职责——认证过程中任何错误抛出后,端点的控制器方法都不会被执行。

5.4 绑定自定义 Sequence 到应用

在application.ts中绑定上述 Sequence:

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

组件源码中同样提供了可参考的完整实现 packages/authentication/src/tests/fixtures/sequences/authentication.sequence.ts,其逻辑与上文一致。

六、认证策略:实现与注册

6.1 统一接口AuthenticationStrategy

AuthenticationStrategy是@loopback/authentication理解的统一接口(定义见 packages/authentication/src/types.ts),任何实现该接口的策略都可直接与认证组件协作——这与 Passport.js 通过统一接口对接众多策略的思路一致:

export interface AuthenticationStrategy { // 'name' 是策略的唯一标识,例如 'basic'、'jwt' name: string; // 'authenticate' 接收请求并返回 UserProfile。 // 凭据有效 → 返回 UserProfile 实例; // 凭据无效 → 抛出错误; // 凭据缺失 → 抛出错误或返回 undefined 交由认证 Action 处理。 authenticate(request: Request): Promise<UserProfile | RedirectRoute | undefined>; }

借助统一的策略接口,以及 extension point/extensions 模式的注册与发现机制,应用可以同时绑定多个策略。组件内置的AuthenticationStrategyProvider会按名称自动发现策略:它使用端点@authenticate()装饰器里给出的名称去查找,再把找到的策略返回给认证 Action 继续执行。

AuthenticationStrategyProvider的源码(packages/authentication/src/providers/auth-strategy.provider.ts)值得细读:它被@extensionPoint('authentication.strategies')标记,且作用域为BindingScope.TRANSIENT(每次请求独立解析,适配请求级上下文)。解析时若在已注册扩展中找不到对应名称的策略,会抛出带AUTHENTICATION_STRATEGY_NOT_FOUND错误码的错误(如The strategy 'xxx' is not available.)。

6.2 注册策略

通常由扩展开发者提供策略类,应用开发者负责注册。以下以仓库中现成的basic策略为例(可参考 basic-strategy.ts 测试夹具)。先把相关文件复制到应用:

  • 将 basic 认证策略 复制到src/strategies/basic-strategy.ts;
  • 将 用户服务 复制到src/services/basic-auth-user-service.ts;
  • 将 用户模型 复制到src/models/user.ts;
  • 将 用户仓库 复制到src/repositories/user.repository.ts。

在application.ts中注册BasicAuthenticationStrategy:

import {registerAuthenticationStrategy} from '@loopback/authentication'; export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); //... registerAuthenticationStrategy(this, BasicAuthenticationStrategy); //... } }

registerAuthenticationStrategy的底层实现(types.ts)调用addExtension,把策略类注册为'authentication.strategies'扩展点的扩展,因此可同时注册任意多个策略。以basic策略的测试夹具为例,其extractCredentials会校验Authorization头的格式与 Base64 解码后的用户名:密码结构,任何一步不合法都抛出HttpErrors.Unauthorized(401)。

提示:上述 Basic 策略在生产中仅作教学示例;仓库另有@loopback/authentication-jwt(见 extensions/authentication-jwt)与 Passport 适配器(见 Authentication-passport)等开箱即用的扩展。

6.3 认证结果的注入与验证

认证成功后,当前用户通过SecurityBindings.USER注入控制器。仓库的验收测试 basic-auth-extension.acceptance.ts 覆盖了完整链路:正确的 Basic 凭据返回用户 ID;缺失Authorization头、头格式错误、解码后缺少:等情况均返回 401;未装饰的端点允许匿名访问;未知策略名返回AUTHENTICATION_STRATEGY_NOT_FOUND;策略返回undefined时返回USER_PROFILE_NOT_FOUND。这些测试是理解组件行为边界的极佳参考。

七、管理自定义认证策略的选项

本小节为可选步骤:若自定义策略无需特殊选项,可跳过。

正如第四节所述,一个设计良好的自定义策略应避免在每个@authenticate装饰器里重复书写默认选项,而应把默认选项集中定义一处,仅在必要时用装饰器传入覆盖选项。

7.1 组件级默认元数据与failOnError

有些场景希望对未显式装饰@authenticate的方法也施加默认认证。可以通过配置认证组件实现(对应AuthenticationOptions.defaultMetadata,见 types.ts):

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

当某个方法配置了多个策略时,可以开启failOnError:设为true后,某个策略可通过抛错来中止整个认证流程;否则认证会继续尝试后续策略,只有所有策略都未成功返回UserProfile(或RedirectRoute)时才判定失败:

app.configure(AuthenticationBindings.COMPONENT).to({failOnError: true});

AuthMetadataProvider(auth-metadata.provider.ts)读取方法/类元数据后,若发现skip标记则返回undefined(放行),否则返回元数据;两者皆无时回退到defaultMetadata。而AuthenticateActionProvider在执行时(auth-action.provider.ts)会遍历所有解析出的策略:某个策略抛错时若failOnError为真则立即抛出,否则收集错误继续尝试下一个;若某个策略返回了RedirectRoute,则把重定向 URL 与状态码绑定到上下文(AUTHENTICATION_REDIRECT_URL/AUTHENTICATION_REDIRECT_STATUS)交给控制器处理重定向;若所有策略都未返回用户档案,则抛出USER_PROFILE_NOT_FOUND。

7.2 定义选项接口与绑定键

为某个策略定义默认选项的接口与绑定键:

export interface AuthenticationStrategyOptions { [property: string]: any; } export namespace BasicAuthenticationStrategyBindings { export const DEFAULT_OPTIONS = BindingKey.create<AuthenticationStrategyOptions>( 'authentication.strategies.basic.defaultoptions', ); }

7.3 绑定默认选项

在application.ts中通过DEFAULT_OPTIONS绑定键绑定默认选项。以下假设策略的默认选项是gatherStatistics: true(真实策略的选项数量可能更多):

export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); //... this.bind(BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS).to({ gatherStatistics: true, }); //... } }

7.4 在装饰器中按需覆盖默认选项

仅在必要时于@authenticate装饰器中指定覆盖选项。下面的示例中,/whoami端点使用默认选项,/scareme端点覆盖为{gatherStatistics: false}:

import {inject} from '@loopback/context'; import {AuthenticationBindings, authenticate} from '@loopback/authentication'; import {UserProfile, securityId} from '@loopback/security'; import {get} from '@loopback/rest'; export class WhoAmIController { constructor( @inject(SecurityBindings.USER) private userProfile: UserProfile, ) {} @authenticate('basic') @get('/whoami') whoAmI(): string { return this.userProfile[securityId]; } @authenticate('basic', {gatherStatistics: false}) @get('/scareme') scareMe(): string { return 'boo!'; } }

7.5 让策略支持默认选项与覆盖

自定义策略需要自行完成"加载默认选项 + 用请求级选项覆盖"的逻辑。更新后的BasicAuthenticationStrategy示例:

import { AuthenticationStrategy, TokenService, AuthenticationMetadata, AuthenticationBindings, } from '@loopback/authentication'; import {UserProfile} from '@loopback/security'; import {Getter} from '@loopback/core'; export interface Credentials { username: string; password: string; } export class BasicAuthenticationStrategy implements AuthenticationStrategy { name: string = 'basic'; // 注入默认选项 @inject(BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS) options: AuthenticationStrategyOptions; constructor( @inject(UserServiceBindings.USER_SERVICE) private userService: UserService, @inject.getter(AuthenticationBindings.METADATA) readonly getMetaData: Getter<AuthenticationMetadata>, ) {} async authenticate(request: Request): Promise<UserProfile | undefined> { const credentials: Credentials = this.extractCredentials(request); await this.processOptions(); if (this.options.gatherStatistics === true) { console.log(`\nGathering statistics...\n`); } else { console.log(`\nNot gathering statistics...\n`); } const user = await this.userService.verifyCredentials(credentials); const userProfile = this.userService.convertToUserProfile(user); return userProfile; } extractCredentials(request: Request): Credentials { // 从 Authorization 头中提取 'basic' 用户凭据的逻辑 return creds; } async processOptions() { // 获取当前请求对应控制器方法的 @authenticate 装饰器 options。 // AuthenticationMetadata 接口包含 strategy: string 与 options?: object, // 这里只关心 options 属性。 const controllerMethodAuthenticationMetadata = await this.getMetaData(); if (!this.options) this.options = {}; // 未绑定默认选项时给空对象 // 用请求级选项覆盖默认选项 this.options = Object.assign( {}, this.options, controllerMethodAuthenticationMetadata.options, ); } }

实现要点有三:

  1. 用BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS绑定键把默认选项注入到属性options;
  2. 用AuthenticationBindings.METADATA绑定键注入一个返回AuthenticationMetadata的getter(命名为getMetaData),该元数据包含传入@authenticate装饰器的参数;
  3. 实现processOptions():取出默认选项,再用装饰器中的请求级覆盖选项合并覆盖;随后在authenticate()中先调用processOptions(),再让策略逻辑响应更新后的选项。

八、完整拼装:一个带认证的 LoopBack 4 应用骨架

综合以上各节,一个接入认证的应用application.ts大致如下:

import { AuthenticationComponent, registerAuthenticationStrategy, } from '@loopback/authentication'; export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); /* 设置其他杂项绑定 */ //... // 加载认证组件 this.component(AuthenticationComponent); // 注册自定义认证策略 registerAuthenticationStrategy(this, BasicAuthenticationStrategy); // 使用自定义认证 Sequence this.sequence(MyAuthenticatingSequence); this.static('/', path.join(__dirname, '../public')); this.projectRoot = __dirname; this.bootOptions = { controllers: { dirs: ['controllers'], extensions: ['.controller.js'], nested: true, }, }; } }

若希望直接获得一个可运行的 JWT 认证示例,仓库提供了完整的教程 Authentication-Tutorial(对应可运行的todo-jwt示例 examples/todo-jwt),其中展示了绑定JWTAuthenticationComponent、在 Sequence 中插入认证 Action、创建登录/注册控制器、并用@authenticate('jwt')保护 Todo API 的完整流程。

九、总结

通过本文,你已经掌握了 LoopBack 4 认证组件的完整脉络:

  • 架构:装饰器 + 元数据 Provider + 认证 Action(含中间件形态)+ 策略扩展点,四者协同完成端点保护;
  • 接入:安装@loopback/authentication、挂载AuthenticationComponent、编写并绑定带认证 Action 的 Sequence(或直接使用中间件式 Sequence);
  • 扩展:实现AuthenticationStrategy接口、用registerAuthenticationStrategy注册策略、通过组件配置(defaultMetadata、failOnError)与策略级默认选项/装饰器覆盖选项来精细控制认证行为;
  • 验证:参考 认证组件的验收测试 与 组件源码 理解各边界行为。

认证之后,如需进一步控制"谁能访问什么资源",可继续阅读 Authorization 相关文档。

  • 后端
  • 微服务

【免费下载链接】loopback-next

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

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载
上一篇:3步解锁Windows安卓生态:WSABuilds完全部署手册
下一篇:go-app内容安全策略:防止XSS和其他安全威胁

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

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

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

立即咨询