
在现代 Spring 应用程序中,Spring WebClient 作为非阻塞、响应式 HTTP 客户端,因其高性能和可伸缩性而受到青睐。然而,当涉及到特定的认证机制,如 Windows NTLM 认证时,WebClient 并没有提供像传统 RestTemplate 结合 Apache HttpClient 那样直接的开箱即用支持。本文将指导您如何在 Spring WebClient 中通过自定义 ExchangeFilterFunction 实现 NTLM 认证。
NTLM 认证机制概述
NTLM(NT LAN Manager)是一种挑战-响应协议,用于在 Windows 环境中进行用户认证。其基本流程涉及客户端发送认证请求,服务器返回一个挑战(Type 2 消息),客户端使用用户的凭据和挑战生成响应(Type 3 消息)并发送回服务器,服务器验证响应以完成认证。这个过程通常需要多步 HTTP 请求才能完成。
实现 NTLM 认证的挑战
与 RestTemplate 可以通过配置 HttpClientBuilder 和 NTCredentials 来轻松集成 NTLM 不同,WebClient 基于 Reactor Netty 或其他响应式 HTTP 客户端,其底层机制不直接暴露 Apache HttpClient 的 NTLM 配置选项。因此,我们需要一种方式来拦截并修改 WebClient 的请求和响应,以模拟 NTLM 的挑战-响应流程。
解决方案:自定义 ExchangeFilterFunction 结合 JCIFS
为了在 WebClient 中实现 NTLM 认证,我们可以利用 ExchangeFilterFunction 接口。该接口允许我们对请求进行预处理,并对响应进行后处理。结合 JCIFS 库(一个 Java 实现的 SMB/CIFS 客户端库,包含 NTLM 认证逻辑),我们可以构建一个自定义的过滤器来处理 NTLM 认证流程。
1. 引入 JCIFS 依赖
首先,确保您的项目中包含 JCIFS 库的依赖。在 Maven 项目中,您可以添加以下依赖:
org.samba.jcifs jcifs 1.3.17
注意: 较新的 jcifs-ng 版本可能提供更好的兼容性和维护,您可以考虑使用 org.samba.jcifs:jcifs-ng:2.1.9 或更高版本。
2. 创建 NtlmAuthorizedClientExchangeFilterFunction
接下来,我们将创建一个名为 NtlmAuthorizedClientExchangeFilterFunction 的类,它实现了 ExchangeFilterFunction 接口。这个过滤器将负责处理 NTLM 的挑战-响应逻辑。
import jcifs.ntlmssp.NtlmFlags;
import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;
import jcifs.util.Base64;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public final class NtlmAuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
private final String domain;
private final String username;
private final String password;
private final int lmCompatibility;
/**
* 构造函数。
* @param domain NTLM 域
* @param username 用户名
* @param password 密码
* @param lmCompatibility LM 兼容性级别 (通常为 3)
*/
public NtlmAuthorizedClientExchangeFilterFunction(String domain, String username, String password, int lmCompatibility) {
this.domain = domain;
this.username = username;
this.password = password;
this.lmCompatibility = lmCompatibility;
// 设置 JCIFS 的 LM 兼容性级别,影响 Type3 消息的生成
System.setProperty("jcifs.smb.lmCompatibility", Integer.toString(lmCompatibility));
}
@Override
public Mono filter(final ClientRequest request, final ExchangeFunction next) {
// NTLM 认证的第一步:发送 Type 1 消息
// Type 1 消息通常不包含凭据,只是协商认证能力
Type1Message type1Message = new Type1Message(NtlmFlags.getDefaultFlags(), domain, username);
String type1Base64 = Base64.encode(type1Message.toByteArray());
return next.exchange(addNtlmHeader(request, type1Base64))
.publishOn(Schedulers.single()) // 确保请求顺序处理,有助于 HTTP Keep-Alive
.flatMap(clientResponse -> {
List ntlmAuthHeaders = getNtlmAuthHeaders(clientResponse);
if (ntlmAuthHeaders.isEmpty()) {
// 如果没有 NTLM 认证头,可能是认证成功或服务器不支持 NTLM
// 或者认证失败,这里需要根据实际业务逻辑处理
// 为了简化,这里假设没有头则认证失败或不需要NTLM
return Mono.just(clientResponse);
}
String ntlmHeader = ntlmAuthHeaders.get(0);
if (ntlmHeader.length() <= 5 || !ntlmHeader.startsWith("NTLM ")) {
// NTLM 认证头格式不正确
return Mono.error(new IllegalStateException("Invalid NTLM WWW-Authenticate header: " + ntlmHeader));
}
try {
// 解析 Type 2 消息 (服务器挑战)
byte[] type2Bytes = Base64.decode(ntlmHeader.substring(5));
Type2Message type2Message = new Type2Message(type2Bytes);
// 根据 Type 2 消息和用户凭据生成 Type 3 消息 (客户端响应)
Type3Message type3Message = new Type3Message(type1Message, type2Message, password, domain, username);
String type3Base64 = Base64.encode(type3Message.toByteArray());
// 发送 Type 3 消息进行最终认证
return next.exchange(addNtlmHeader(request, type3Base64));
} catch (IOException e) {
return Mono.error(new RuntimeException("Failed to process NTLM authentication", e));
}
});
}
/**
* 从 ClientResponse 中提取 NTLM 认证头。
* 通常是 WWW-Authenticate 头,以 "NTLM " 开头。
* @param clientResponse 客户端响应
* @return 包含 NTLM 认证头的列表
*/
private static List getNtlmAuthHeaders(ClientResponse clientResponse) {
List wwwAuthHeaders = clientResponse.headers().header(HttpHeaders.WWW_AUTHENTICATE);
// 过滤出以 "NTLM" 开头的头,并按长度排序(通常 Type 2 消息的头更长)
return wwwAuthHeaders.stream()
.filter(h -> h.startsWith("NTLM"))
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
}
/**
* 向 ClientRequest 中添加 NTLM Authorization 头。
* @param clientRequest 原始请求
* @param ntlmPayloadBase64 NTLM 消息的 Base64 编码字符串
* @return 带有 NTLM Authorization 头的新请求
*/
private ClientRequest addNtlmHeader(ClientRequest clientRequest, String ntlmPayloadBase64) {
return ClientRequest.from(clientRequest)
.header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(ntlmPayloadBase64))
.build();
}
} 核心代码解析:
- 构造函数: 接收 NTLM 认证所需的 domain、username、password 和 lmCompatibility。lmCompatibility 参数对于 NTLMv2 认证非常重要,通常设置为 3。
-
filter 方法: 这是 ExchangeFilterFunction 的核心。
- Type 1 消息: 首先,构造一个 Type1Message(NTLM 协商消息),将其 Base64 编码后作为 Authorization 头发送。这是一个不包含凭据的初步请求。
-
接收 Type 2 消息: 服务器收到 Type 1 消息后,如果需要 NTLM 认证,会返回一个包含 WWW-Authenticate: NTLM
的响应(通常是 401 Unauthorized)。过滤器会从响应头中解析出这个 Type 2 消息。 - 生成 Type 3 消息: 客户端使用接收到的 Type2Message、用户凭据(域名、用户名、密码)和 Type 1 消息来生成 Type3Message(认证响应消息)。
- 发送 Type 3 消息: 将 Type 3 消息 Base64 编码后,再次作为 Authorization 头发送给服务器。这次请求包含了认证信息,服务器会对其进行验证。
- publishOn(Schedulers.single()): 这一行非常重要。NTLM 认证是一个多步过程,通常依赖于 HTTP Keep-Alive 来保持同一个连接。使用 Schedulers.single() 可以确保这些认证步骤在同一个线程上按顺序执行,从而更好地利用底层 HTTP 客户端的连接池和 Keep-Alive 机制。
-
辅助方法:
- getNtlmAuthHeaders:从响应头中筛选出以 "NTLM" 开头的 WWW-Authenticate 头。
- addNtlmHeader:将 NTLM 认证信息添加到请求的 Authorization 头中。
3. 将过滤器集成到 WebClient
在您的 Spring 应用程序中,您可以像这样构建 WebClient 实例并应用这个自定义过滤器:
import org.springframework.web.reactive.function.client.WebClient;
public class WebClientNtlmConfig {
public WebClient createNtlmWebClient(String domain, String username, String password, int lmCompatibility) {
NtlmAuthorizedClientExchangeFilterFunction ntlmFilter =
new NtlmAuthorizedClientExchangeFilterFunction(domain, username, password, lmCompatibility);
return WebClient.builder()
.filter(ntlmFilter) // 添加 NTLM 认证过滤器
// 其他配置,如 baseUrl、默认头等
// .baseUrl("https://your-ntlm-protected-service.com")
.build();
}
public static void main(String[] args) {
WebClientNtlmConfig config = new WebClientNtlmConfig();
WebClient webClient = config.createNtlmWebClient(
"MY_DOMAIN", // 替换为您的 NTLM 域
"my_user", // 替换为您的用户名
"my_password", // 替换为您的密码
3 // LM 兼容性级别,通常为 3
);
// 使用配置好的 WebClient 发送请求
webClient.get()
.uri("https://my.url.com/api/resource") // 替换为您的 NTLM 保护的资源 URL
.retrieve()
.bodyToMono(String.class)
.doOnNext(response -> System.out.println("Response: " + response))
.doOnError(error -> System.err.println("Error: " + error.getMessage()))
.block(); // 在实际应用中避免使用 block()
}
}注意事项与限制
- 错误处理: 提供的 NtlmAuthorizedClientExchangeFilterFunction 示例中,对于错误情况(如解析 NTLM 头失败、IOException 等)使用了 Mono.error(...)。在生产环境中,您需要实现更健壮的错误处理逻辑,例如记录日志、返回特定的错误响应或重试机制。
- LM 兼容性级别: lmCompatibility 参数对于 NTLMv2 认证至关重要。建议将其设置为 3,以支持 NTLMv2 并禁用 LM 和 NTLMv1。
- 性能: NTLM 认证涉及多次网络往返和 Base64 编解码,相比其他认证方式(如 Basic Auth 或 Bearer Token)会有一定的性能开销。
- 无凭据 NTLM 认证(当前用户上下文): 针对问题中提到的“在 Windows 环境下使用当前用户上下文进行 NTLM 认证,无需提供用户名/密码”的需求,此方案无法直接支持。JCIFS 库主要用于通过显式凭据进行 NTLM 认证。要在 Java 中实现基于当前 Windows 用户上下文的认证,通常需要依赖操作系统级别的 API 或特定的 JVM 实现(如使用 sun.security.jgss.GSSUtil 结合 Kerberos),这超出了本教程的范围,并且通常具有平台依赖性。
- 安全性: 在生产环境中,不应将凭据硬编码。应通过安全的方式(如环境变量、Spring Cloud Config、Vault 等)注入用户名和密码。
- WebClient 的底层 HTTP 客户端: Spring WebClient 默认使用 Reactor Netty。此 ExchangeFilterFunction 的实现是通用的,不依赖于特定的底层 HTTP 客户端,但 publishOn(Schedulers.single()) 对于确保请求顺序执行和 HTTP Keep-Alive 的有效性非常关键。
总结
通过自定义 ExchangeFilterFunction 并利用 JCIFS 库,我们成功地为 Spring WebClient 实现了 Windows NTLM 认证。这种方法虽然比 RestTemplate 复杂一些,但它与 WebClient 的响应式编程模型无缝集成,允许您在现代 Spring 应用中访问 NTLM 保护的资源。在实现过程中,需要特别注意 NTLM 的挑战-响应流程、lmCompatibility 设置以及 publishOn(Schedulers.single()) 的使用,以确保认证的正确性和效率。对于无需显式凭据的当前用户上下文认证,则需要考虑其他平台特定的解决方案。










