在现代 Spring 应用程序中,Spring WebClient 作为非阻塞、响应式 HTTP 客户端,因其高性能和可伸缩性而受到青睐。然而,当涉及到特定的认证机制,如 Windows NTLM 认证时,WebClient 并没有提供像传统 RestTemplate 结合 Apache HttpClient 那样直接的开箱即用支持。本文将指导您如何在 Spring WebClient 中通过自定义 ExchangeFilterFunction 实现 NTLM 认证。
NTLM(NT LAN Manager)是一种挑战-响应协议,用于在 Windows 环境中进行用户认证。其基本流程涉及客户端发送认证请求,服务器返回一个挑战(Type 2 消息),客户端使用用户的凭据和挑战生成响应(Type 3 消息)并发送回服务器,服务器验证响应以完成认证。这个过程通常需要多步 HTTP 请求才能完成。
与 RestTemplate 可以通过配置 HttpClientBuilder 和 NTCredentials 来轻松集成 NTLM 不同,WebClient 基于 Reactor Netty 或其他响应式 HTTP 客户端,其底层机制不直接暴露 Apache HttpClient 的 NTLM 配置选项。因此,我们需要一种方式来拦截并修改 WebClient 的请求和响应,以模拟 NTLM 的挑战-响应流程。
为了在 WebClient 中实现 NTLM 认证,我们可以利用 ExchangeFilterFunction 接口。该接口允许我们对请求进行预处理,并对响应进行后处理。结合 JCIFS 库(一个 Java 实现的 SMB/CIFS 客户端库,包含 NTLM 认证逻辑),我们可以构建一个自定义的过滤器来处理 NTLM 认证流程。
首先,确保您的项目中包含 JCIFS 库的依赖。在 Maven 项目中,您可以添加以下依赖:
org.samba.jcifs jcifs1.3.17
注意: 较新的 jcifs-ng 版本可能提供更好的兼容性和维护,您可以考虑使用 org.samba.jcifs:jcifs-ng:2.1.9 或更高版本。
接下来,我们将创建一个名为 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(); } }
在您的 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()
}
}通过自定义 ExchangeFilterFunction 并利用 JCIFS 库,我们成功地为 Spring WebClient 实现了 Windows NTLM 认证。这种方法虽然比 RestTemplate 复杂一些,但它与 WebClient 的响应式编程模型无缝集成,允许您在现代 Spring 应用中访问 NTLM 保护的资源。在实现过程中,需要特别注意 NTLM 的挑战-响应流程、lmCompatibility 设置以及 publishOn(Schedulers.single()) 的使用,以确保认证的正确性和效率。对于无需显式凭据的当前用户上下文认证,则需要考虑其他平台特定的解决方案。