Apache Druid 认证与授权配置指南:Authenticator 链、Authorizer 与 Escalator 深度解析

原创2026-09-22 17:32:091,167 阅读
文章标签:数据库OLAP大数据后端

Apache Druid 认证与授权配置指南:Authenticator 链、Authorizer 与 Escalator 深度解析

本文以 Apache Druid 官方运维文档 docs/operations/auth.md 为骨架,系统讲解 Druid 中与扩展无关(non-extension specific)的认证(Authentication)与授权(Authorization)基础配置。你将掌握 druid.auth.authenticatorChain、druid.auth.authorizers、druid.escalator.type 等核心属性的含义与用法,理解三种内置 Authenticator(AllowAll、Anonymous、Trusted Domain)的适用场景,以及 Druid 进程间内部通信如何通过 Escalator 与"内部系统用户"完成提权认证;文章同时结合 server 模块源码揭示这些配置背后的接口契约与 Filter 级实现原理,为后续接入 druid-basic-security、druid-kerberos 等安全扩展打下基础。

核心配置总览

Druid 的认证/授权基础能力完全通过 JVM 配置属性开启,与具体的认证方案(Basic、Kerberos、LDAP 等)解耦——具体认证方案由扩展提供。以下是 docs/operations/auth.md 给出的核心配置属性:

属性 类型 说明 默认值 是否必填
druid.auth.authenticatorChain JSON 字符串列表 Authenticator 类型名列表,定义认证链 ["allowAll"] 否
druid.escalator.type String 用于 Druid 内部通信的 Escalator 类型。该 Escalator 使用的认证方案必须被 druid.auth.authenticatorChain 中某个 Authenticator 支持 "noop" 否
druid.auth.authorizers JSON 字符串列表 Authorizer 类型名列表,定义授权器 ["allowAll"] 否
druid.auth.unsecuredPaths 字符串列表 不做安全校验的路径列表,对这些路径的所有请求直接放行 [] 否
druid.auth.allowUnauthenticatedHttpOptions Boolean 若为 true,允许未认证用户发送 HTTP OPTIONS 请求。主要用于支持 CORS 预检请求(Druid 本身不直接支持 CORS,可通过第三方扩展开启) false 否

关于 druid.auth.allowUnauthenticatedHttpOptions,有两个必须注意的配套条件与安全影响:

  • 必须同时在 druid.server.http.allowedHttpMethods 中加入 "OPTIONS",否则放行逻辑无法生效;
  • 关闭 OPTIONS 请求的认证检查后,未认证用户可以通过判断 OPTIONS 请求返回 200 而非 404 来探测 Druid 有效端点,进而泄露服务器配置信息(包括加载了哪些扩展),官方文档明确提示 Enabling this option will reveal information about server configuration,开启需谨慎。

druid.auth.unsecuredPaths 则常用于对健康检查、状态页等端点放行,例如 ["/status/health", "/status"],使其不进入认证/授权流程。

Authenticator 认证链工作原理

认证决策由一串 Authenticator 实例(chain)按序处理,顺序由 druid.auth.authenticatorChain 定义。具体实现由扩展提供,例如启用 druid-kerberos 与 druid-basic-security 两个核心扩展中的 Kerberos 与 HTTP Basic 认证器:

druid.auth.authenticatorChain=["kerberos", "basic"]

认证链的请求处理遵循以下规则:

  1. 请求依次经过链上所有 Authenticator,直到某个 Authenticator 成功完成认证,或某个 Authenticator 直接返回 HTTP 错误响应为止;
  2. 一旦出现首次认证成功或错误响应,链上后续 Authenticator 将被跳过;
  3. 若整个链走完仍无人成功认证、也无错误响应,则在链尾统一发送 HTTP 错误响应。

从源码层面看,Authenticator 接口本身继承自 ServletFilterHolder,其 getFilter() 返回的正是执行认证检查的 Servlet Filter(见 Authenticator.java)。接口契约在 Authenticator.java 中明确了三种情形:

  • 认证成功:在请求上设置 Druid-Authentication-Result 属性,内容为携带请求者身份与 Authorizer 名称的 AuthenticationResult;
  • 请求格式无法识别:Filter 不应发送错误响应,将机会留给链上后续 Filter,若整个链耗尽则发送 challenge 响应;
  • 认证失败(识别了方案但凭据错误):Filter 应直接发送错误响应,不再进入后续 Filter。

同时接口还定义了 getAuthChallengeHeader()(返回如 Basic、Negotiate 这样的 WWW-Authenticate 挑战头)与 authenticateJDBCContext()(用于对 Druid SQL 的 JDBC 连接上下文做认证)两个扩展点(Authenticator.java)。

Druid 内置三个 Authenticator 实现,类型名常量定义在 AuthConfig.java:allowAll、anonymous、trustedDomain,并通过 @JsonSubTypes 注册到 Authenticator 接口上(Authenticator.java)。

AllowAll 认证器

内置的 allowAll 认证器对所有请求一律认证通过,并总是将请求路由到名为 allowAll 的 Authorizer。官方文档明确指出它只应用于默认的未安全配置(unsecured configuration),不要在生产安全环境中使用。

druid.auth.authorizer.allowAll.type=allowAll

源码佐证:AllowAllAuthenticator.java 中定义了静态常量 ALLOW_ALL_RESULT,其身份、Authorizer 名称均为 "allowAll";其 Filter 的 doFilter 逻辑只是无条件设置 Druid-Authentication-Result 属性后放行(AllowAllAuthenticator.java)。

Anonymous 认证器

内置的 anonymous 认证器同样认证所有请求,但将请求路由到用户配置指定的 Authorizer,适用于为集群增加一个"默认访问级别"。官方建议将其放在认证链的末尾:当请求到达链尾的 Anonymous 认证器时,能否成功取决于其所关联 Authorizer 的授权结果。

其配置属性如下:

属性 说明 默认值 是否必填
druid.auth.authenticator.<authenticatorName>.authorizerName 请求应被路由到的 Authorizer 名称 N/A 是
druid.auth.authenticator.<authenticatorName>.identity 请求者的身份标识 defaultUser 否

使用方式是在认证链中加入类型为 anonymous 的认证器。例如配合 druid-basic-security 扩展(extensions-core/druid-basic-security):

druid.auth.authenticatorChain=["basic", "anonymous"]

druid.auth.authenticator.anonymous.type=anonymous
druid.auth.authenticator.anonymous.identity=defaultUser
druid.auth.authenticator.anonymous.authorizerName=myBasicAuthorizer

# ... usual configs for basic authentication would go here ...

源码佐证:AnonymousAuthenticator.java 中 DEFAULT_IDENTITY 为 "defaultUser",构造函数通过 @JsonProperty 接收 name、authorizerName、identity,identity 缺省时使用 defaultUser;其 Filter 无条件设置 AuthenticationResult 后放行。

Trusted Domain 认证器

内置的 trustedDomain 认证器用于信任同一域内主机:当请求来源 IP 属于配置的可信域时认证通过,并路由到用户指定的 Authorizer。适合为同域内主机提供默认信任级别。

其配置属性如下:

属性 说明 默认值 是否必填
druid.auth.authenticator.<authenticatorName>.name 认证器名称 N/A 是
druid.auth.authenticator.<authenticatorName>.domain 可信域,请求来源需属于该域才被认证。若只允许来自某台主机的连接,需填写该主机的完整域名(FQDN) N/A 是
druid.auth.authenticator.<authenticatorName>.useForwardedHeaders 客户端可能经过多层代理,部分代理会在转发前把自身 IP 追加进 X-Forwarded-For 头。设为 true 时,若存在 X-Forwarded-For,则使用其中最左侧的主机名进行域匹配。注意:HTTP 请求中的 X-Forwarded-For 头可被伪造,启用需谨慎 false 否
druid.auth.authenticator.<authenticatorName>.authorizerName 请求应被路由到的 Authorizer 名称 N/A 是
druid.auth.authenticator.<authenticatorName>.identity 请求者的身份标识 defaultUser 否

使用方式是在认证链中加入类型为 trustedDomain 的认证器:

druid.auth.authenticatorChain=["trustedDomain"]

druid.auth.authenticator.trustedDomain.type=trustedDomain
druid.auth.authenticator.trustedDomain.domain=trustedhost.mycompany.com
druid.auth.authenticator.trustedDomain.identity=defaultUser
druid.auth.authenticator.trustedDomain.authorizerName=myBasicAuthorizer
druid.auth.authenticator.trustedDomain.name=myTrustedAutenticator
# ... usual configs for druid would go here ...

源码实现细节(TrustedDomainAuthenticator.java):

  • 构造函数用 Preconditions.checkArgument(!Strings.isNullOrEmpty(domain), ...) 强制要求 domain 非空(TrustedDomainAuthenticator.java);
  • 域匹配逻辑是 remoteAddr.endsWith(domain),即请求来源地址以配置域名为后缀即视为可信(TrustedDomainAuthenticator.java);
  • 启用 useForwardedHeaders 时,从 X-Forwarded-For 头取最左侧(原始客户端)地址,处理方式为 forwarded_for.split(",")<a href="https://link.gitcode.com/i/4ffa8c53bbe9ef038f115d45270c1a8c" target="_blank">0]([TrustedDomainAuthenticator.java);
  • 匹配失败时不会设置认证结果属性,而是继续走 Filter 链(把机会留给链上后续认证器),认证器本身不发送错误响应(TrustedDomainAuthenticator.java)。

对应单元测试 TrustedDomainAuthenticatorTest.java 覆盖了四类场景,可据此验证行为:

Escalator:内部集群通信的认证提升

druid.escalator.type 决定 Druid 进程间内部通信(例如 Broker 在处理查询时与 Historical 通信)所使用的认证方案。关键约束是:所选 Escalator 的认证方案必须被 druid.auth.authenticatorChain 中某个 Authenticator 支持;同理,Authenticator 扩展实现者若希望其方案可用于内部通信,也必须同时提供对应的 Escalator 实现。

Escalator 接口定义在 Escalator.java,并通过 @JsonTypeInfo(..., defaultImpl = NoopEscalator.class) 将 noop 设为默认实现(Escalator.java)。接口核心方法:

HttpClient createEscalatedClient(HttpClient baseClient);
AuthenticationResult createEscalatedAuthenticationResult();
  • createEscalatedClient:返回一个包装后的 HttpClient,该客户端在请求中自动携带"内部系统用户"的凭据,用于 Druid 节点间通信(如 Broker 与 Historical 之间的查询执行);
  • createEscalatedAuthenticationResult:返回代表"内部系统用户"身份的 AuthenticationResult。

说明:原文档中还提及 createEscalatedJettyClient(org.eclipse.jetty.client.HttpClient baseClient) 这一针对 Jetty HttpClient 的变体方法。当前仓库 server 模块的 Escalator 接口声明了两个核心方法(见上),Jetty 变体由扩展实现根据自身需求提供,这是接口演进与扩展自主性的体现。

Noop Escalator

内置默认的 noop Escalator 专为默认的 AllowAll Authenticator/Authorizer 设计,仅适用于未安全配置。其实现非常直白(NoopEscalator.java):

  • createEscalatedClient 直接返回原始 baseClient,不做任何包装(NoopEscalator.java);
  • createEscalatedAuthenticationResult 直接返回 AllowAllAuthenticator.ALLOW_ALL_RESULT,即身份为 allowAll(NoopEscalator.java)。

Authorizer 授权器

授权决策由 Authorizer 负责,druid.auth.authorizers 属性决定启用的 Authorizer 实现。内置实现有两个:default 与 noop,其余由扩展提供。例如启用 druid-basic-security 的 basic 授权器:

druid.auth.authorizers=["basic"]

任意一个请求只会由一个 Authorizer 完成授权(Only a single Authorizer will authorize any given request)。

Authorizer 接口非常精简(Authorizer.java):

Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action);

其语义是:判断 authenticationResult 所代表的身份是否有权对 resource 执行 action,返回不可为空的 Access 对象。每个节点每种 Authorizer 实现只创建一个实例;安全敏感端点会从请求的 Druid-Auth-Token 属性中取出身份字符串,交给 Authorizer 做 authorize() 检查(见 Authorizer.java 的接口注释)。

AllowAll 授权器

类型名为 allowAll 的内置 Authorizer 接受所有请求。其实现只是直接返回 Access.OK(AllowAllAuthorizer.java),与 AllowAll 认证器一样仅用于默认未安全配置。

默认未安全配置(Default Unsecured Configuration)

当上述核心属性缺省时,Druid 会自动落到"全放行"的默认配置,三者必须保持配套:

  • druid.auth.authenticatorChain 为空或未指定时,Druid 创建单个名为 allowAll 的 AllowAll Authenticator 组成的认证链;
  • druid.auth.authorizers 为空或未指定时,Druid 创建单个名为 allowAll 的 AllowAll Authorizer;
  • druid.escalator.type 默认为 "noop",与默认的 AllowAll Authenticator/Authorizer 配置匹配。

因此,未做任何安全配置的 Druid 集群,其所有 HTTP 端点对所有人开放,仅适合开发环境或完全可信的内网。

Authenticator 到 Authorizer 的路由

当一个 Authenticator 成功认证请求后,它必须在请求上挂载一个 AuthenticationResult,其中包含两部分关键信息:

  1. 请求者的身份信息(identity);
  2. 应执行授权检查的 Authorizer 名称。

Authenticator 实现应通过配置项(如上文 Anonymous/Trusted Domain 认证器中的 authorizerName)让用户选择该 Authenticator 将请求路由到哪个 Authorizer,从而形成"认证方案 + 授权策略"的灵活组合。例如 druid-basic-security 中通常的做法是一个 Basic 认证器配合一个存储了用户权限映射的 Basic 授权器。

内部系统用户(Internal System User)

Druid 进程之间(非用户发起的通信)发起的内部请求也必须携带认证凭据。这类请求应使用一个代表 Druid 集群本身的身份——"内部系统用户",并拥有全部访问权限。具体如何定义该用户由扩展实现决定,但所有参与方必须遵守以下契约:

  • Authorizer 侧:授权器实现必须能够识别并授权"内部系统用户"身份,赋予其全部访问权限;
  • Authenticator 侧:凡是宣称支持 Druid 内部通信的 Authenticator 实现,必须能够识别对应 Escalator 实现所提供的"内部系统用户"凭据;
  • Escalator 侧:负责在内部通信中注入该用户的凭据与身份,通过上文 createEscalatedClient / createEscalatedAuthenticationResult(以及扩展可选的 Jetty 变体)完成。

这套机制保证了:外部用户认证走认证链 + 授权器,内部节点通信则通过 Escalator "提升"为集群自身的特权身份,二者互不干扰。

扩展实现者注意事项:保留配置属性名

对于扩展实现者,官方文档特别提醒以下配置属性被保留用于 Authenticator 与 Authorizer 的名称注入:

druid.auth.authenticator.<authenticator-name>.name=<authenticator-name>
druid.auth.authorizer.<authorizer-name>.name=<authorizer-name>

这些属性以 @JsonProperty 参数形式将认证器/授权器名称传递给实现,在同一类型配置了多个实例时尤为有用——例如两个同为 basic 类型的授权器,可通过 name 区分彼此。从源码看,AnonymousAuthenticator、TrustedDomainAuthenticator 的构造函数确实都以 @JsonProperty("name") String name 接收该值,并把它写入 AuthenticationResult(见 AnonymousAuthenticator.java、TrustedDomainAuthenticator.java)。

相关资源

实践建议:生产环境不要停留在本文所述的默认 AllowAll 配置上,应至少启用 druid-basic-security(HTTP Basic + 权限映射)并配置 druid.escalator.type 为与之匹配的方案(如 basic 扩展提供的 Escalator),同时用 druid.auth.unsecuredPaths 放行健康检查端点、按需评估 druid.auth.allowUnauthenticatedHttpOptions 的风险后再开启。

登录后查看全文
druid