
在现代 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 项目中,您可以添加以下依赖:
<dependency>
    <groupId>org.samba.jcifs</groupId>
    <artifactId>jcifs</artifactId>
    <version>1.3.17</version> <!-- 或更高版本,请根据实际情况选择 -->
</dependency>注意: 较新的 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<ClientResponse> 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<String> 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<String> getNtlmAuthHeaders(ClientResponse clientResponse) {
        List<String> 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()) 的使用,以确保认证的正确性和效率。对于无需显式凭据的当前用户上下文认证,则需要考虑其他平台特定的解决方案。
以上就是在 Spring WebClient 中实现 Windows NTLM 认证的详细内容,更多请关注php中文网其它相关文章!
                        
                        每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
                Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号