0

0

Spring WebClient实现Windows NTLM认证的专业指南

心靈之曲

心靈之曲

发布时间:2025-10-28 12:09:22

|

790人浏览过

|

来源于php中文网

原创

spring webclient实现windows ntlm认证的专业指南

在现代企业级应用中,与依赖Windows NTLM认证的后端服务进行交互是常见需求。然而,Spring Framework的响应式Web客户端——WebClient,不像其前身RestTemplate那样直接支持NTLM认证,这给开发者带来了一定的挑战。本文将详细介绍如何通过自定义ExchangeFilterFunction并结合JCIFS库,为WebClient实现健壮的Windows NTLM认证机制。

1. NTLM认证机制概述与WebClient的挑战

NTLM(NT LAN Manager)是一种挑战-响应(Challenge-Response)协议,用于验证用户身份。其基本流程涉及客户端发送认证请求(Type 1消息),服务器返回挑战(Type 2消息),客户端根据挑战和用户凭据计算响应(Type 3消息)并发送给服务器,最终服务器验证响应。

RestTemplate可以通过配置HttpClient(如Apache HttpClient)并使用NTCredentials来相对容易地实现NTLM认证。然而,WebClient通常默认使用Reactor Netty作为底层HTTP客户端,且其ExchangeFilterFunctions主要针对Basic认证等更简单的机制。直接使用basicAuthentication或手动设置Authorization头并不能满足NTLM的挑战-响应流程。因此,我们需要一个能够拦截请求和响应,并根据NTLM协议进行多步处理的自定义过滤器。

2. 基于JCIFS的自定义NTLM认证过滤器实现

为了在WebClient中实现NTLM认证,我们可以利用JCIFS库,它提供了NTLM协议的Java实现。核心思想是创建一个ExchangeFilterFunction,它能够:

  1. 在初始请求中发送NTLM Type 1消息。
  2. 捕获服务器返回的NTLM Type 2挑战。
  3. 根据Type 2挑战和用户凭据生成NTLM Type 3响应,并重新发送请求。

以下是实现NtlmAuthorizedClientExchangeFilterFunction的详细代码:

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;

/**
 * Spring WebClient的NTLM认证过滤器。
 * 使用JCIFS库实现NTLM挑战-响应机制。
 */
public final class NtlmAuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {

    private final String domain;
    private final String username;
    private final String password;
    private final boolean doSigning;
    private final int lmCompatibility;

    /**
     * 构造函数。
     * @param domain NTLM域
     * @param username 用户名
     * @param password 密码
     * @param doSigning 是否进行消息签名(推荐为true以增强安全性)
     * @param lmCompatibility LM兼容性级别 (0-5),影响密码哈希算法
     */
    public NtlmAuthorizedClientExchangeFilterFunction(String domain, String username, String password, boolean doSigning, int lmCompatibility) {
        this.domain = domain;
        this.username = username;
        this.password = password;
        this.doSigning = doSigning;
        this.lmCompatibility = lmCompatibility;
        // 设置JCIFS的LM兼容性系统属性
        System.setProperty("jcifs.smb.lmCompatibility", Integer.toString(lmCompatibility));
    }

    @Override
    public Mono filter(final ClientRequest request, final ExchangeFunction next) {
        // NTLM认证需要状态,因此在每次请求中创建一个新的上下文
        // NTLM上下文的标志,包括请求签名和NTLMSSP_NEGOTIATE_ALWAYS_SIGN
        int flags = NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE |
                    NtlmFlags.NTLMSSP_NEGOTIATE_OEM |
                    NtlmFlags.NTLMSSP_REQUEST_TARGET |
                    NtlmFlags.NTLMSSP_NEGOTIATE_NTLM;
        if (doSigning) {
            flags |= NtlmFlags.NTLMSSP_NEGOTIATE_SIGN | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN;
        }

        try {
            // 第一步:发送Type 1消息
            Type1Message type1 = new Type1Message(flags, domain, null); // workstation留空,JCIFS会自动处理
            byte[] type1Bytes = type1.toByteArray();

            return next.exchange(addNtlmHeader(request, type1Bytes))
                // 确保请求按顺序处理,以维持HTTP连接和状态
                .publishOn(Schedulers.single())
                .flatMap(clientResponse -> {
                    // 检查响应是否包含NTLM挑战
                    List ntlmAuthHeaders = getNtlmAuthHeaders(clientResponse);
                    if (ntlmAuthHeaders.isEmpty()) {
                        // 如果没有NTLM挑战,则可能是认证成功或非NTLM认证,直接返回响应
                        // 或者根据业务需求抛出错误
                        return Mono.just(clientResponse);
                    }

                    // 提取Type 2消息
                    String ntlmHeader = ntlmAuthHeaders.get(0);
                    if (ntlmHeader.length() <= 5) { // "NTLM " + base64 content
                        return Mono.error(new IOException("Invalid NTLM challenge header: " + ntlmHeader));
                    }
                    try {
                        byte[] type2Bytes = Base64.decode(ntlmHeader.substring(5));
                        Type2Message type2 = new Type2Message(type2Bytes);

                        // 第二步:根据Type 2消息和凭据生成Type 3消息
                        Type3Message type3 = new Type3Message(type2, password, domain, username);
                        byte[] type3Bytes = type3.toByteArray();

                        // 重新发送带有Type 3消息的请求
                        return next.exchange(addNtlmHeader(request, type3Bytes));
                    } catch (IOException e) {
                        return Mono.error(new RuntimeException("Failed to process NTLM Type 2 message or generate Type 3 message", e));
                    }
                });
        } catch (IOException e) {
            return Mono.error(new RuntimeException("Failed to generate NTLM Type 1 message", e));
        }
    }

    /**
     * 从ClientResponse中提取NTLM认证头。
     * @param clientResponse 客户端响应
     * @return 包含"NTLM"前缀的WWW-Authenticate头列表
     */
    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).reversed()) // 优先处理更长的NTLM头
            .collect(Collectors.toList());
    }

    /**
     * 向请求中添加NTLM认证头。
     * @param clientRequest 原始请求
     * @param ntlmPayload NTLM消息的字节数组
     * @return 添加了认证头的新请求
     */
    private ClientRequest addNtlmHeader(ClientRequest clientRequest, byte[] ntlmPayload) {
        return ClientRequest.from(clientRequest)
            .header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(ntlmPayload)))
            .build();
    }
}

2.1 代码详解

  • 构造函数: 接收domain、username、password、doSigning和lmCompatibility作为参数。doSigning控制是否启用NTLM消息签名,这对于安全性非常重要。lmCompatibility是一个JCIFS特有的设置,影响密码哈希算法,通常根据目标NTLM服务器的配置进行调整。
  • filter方法: 这是ExchangeFilterFunction的核心。
    • Type 1消息发送: 首先构建一个Type1Message(初始认证请求),将其转换为字节数组并Base64编码,然后作为Authorization头(NTLM )添加到原始请求中。
    • publishOn(Schedulers.single()): 关键点。NTLM认证是一个有状态的协议,需要确保同一个HTTP连接用于后续的挑战-响应。publishOn(Schedulers.single())确保了对next.exchange的调用在同一个线程上顺序执行,有助于维持HTTP连接的活性(Keep-Alive)和状态一致性。
    • 响应处理与Type 2消息提取: 在收到第一个响应后,过滤器检查WWW-Authenticate头是否包含NTLM挑战。如果包含,它会提取Base64编码的Type 2消息。
    • Type 3消息生成与重新发送: 使用提取的Type 2消息、用户的密码、域和用户名来生成Type3Message(认证响应)。同样,将其转换为字节数组并Base64编码,作为Authorization头添加到原始请求中,并重新发起请求。
  • getNtlmAuthHeaders: 辅助方法,用于从WWW-Authenticate头中过滤出所有以"NTLM"开头的认证头。通过对长度进行降序排序,可以优先处理包含Type 2消息的更长的头。
  • addNtlmHeader: 辅助方法,用于构建新的ClientRequest,并添加带有NTLM payload的Authorization头。

3. 集成自定义过滤器到WebClient

要使用上述自定义NTLM过滤器,您需要将其添加到WebClient.builder()的过滤器链中:

ClipDrop
ClipDrop

Stability.AI出品的图片处理系列工具(背景移除、图片放大、打光)

下载
import org.springframework.web.reactive.function.client.WebClient;

public class NtlmWebClientConfig {

    public WebClient ntlmAuthenticatedWebClient() {
        String domain = "YOUR_DOMAIN"; // 例如 "MYDOMAIN"
        String username = "YOUR_USERNAME";
        String password = "YOUR_PASSWORD";
        boolean doSigning = true; // 推荐开启消息签名
        int lmCompatibility = 3; // 根据NTLM服务器配置调整,常见值如3

        NtlmAuthorizedClientExchangeFilterFunction ntlmFilter =
            new NtlmAuthorizedClientExchangeFilterFunction(domain, username, password, doSigning, lmCompatibility);

        return WebClient.builder()
            .filter(ntlmFilter)
            // 可以添加其他过滤器或配置
            .baseUrl("https://my.ntlm.protected.service")
            .build();
    }

    public static void main(String[] args) {
        NtlmWebClientConfig config = new NtlmWebClientConfig();
        WebClient webClient = config.ntlmAuthenticatedWebClient();

        webClient.get()
            .uri("/some/resource")
            .retrieve()
            .bodyToMono(String.class)
            .doOnNext(System.out::println)
            .doOnError(e -> System.err.println("Error: " + e.getMessage()))
            .block(); // 阻塞以等待结果,实际应用中通常使用订阅
    }
}

3.1 依赖管理

为了使上述代码正常工作,您需要在项目的pom.xml(Maven)或build.gradle(Gradle)中添加JCIFS库的依赖:

Maven:


    jcifs
    jcifs
    1.3.17 

Gradle:

implementation 'jcifs:jcifs:1.3.17' // 请检查最新稳定版本

注意: JCIFS库的最新版本可能在Maven中央仓库中有所变动,请查阅官方文档或Maven Central以获取最新稳定版本。

4. 注意事项与限制

  • 凭据管理: 示例代码中直接在代码中硬编码了用户名和密码。在生产环境中,这些凭据应通过安全的方式(如环境变量、Vault、Spring Cloud Config等)进行管理和注入。
  • lmCompatibility: 这个参数非常重要。不同的NTLM服务器对LM兼容性级别有不同的要求。如果设置不正确,可能导致认证失败。通常,3是一个比较通用的值,但可能需要根据实际环境进行调整。
  • 消息签名 (doSigning): 启用消息签名(doSigning = true)可以增强NTLM认证的安全性,防止中间人攻击篡改消息。建议在生产环境中开启。
  • 错误处理: 示例代码中的错误处理相对简单。在实际应用中,应添加更健壮的错误日志和异常处理逻辑,例如区分认证失败、网络错误等。
  • 当前用户上下文认证: 关于在Windows环境下不提供用户名和密码,而是使用当前运行进程的用户上下文进行NTLM认证的需求,这是一个更复杂的场景。JCIFS库本身可能不直接支持这种"无凭据"的认证方式,因为它通常需要明确的用户名、密码和域。这种需求通常依赖于底层的操作系统API(如SSPI),这超出了纯Java库的范畴,并且在跨平台环境中实现起来非常困难。对于WebClient而言,目前没有直接的、通用的解决方案来利用操作系统的当前用户上下文进行NTLM认证。如果这是强制要求,可能需要考虑使用JNI/JNA调用Windows SSPI API,或者寻找其他支持此功能的特定HTTP客户端库。

5. 总结

通过实现自定义的ExchangeFilterFunction并结合JCIFS库,我们成功地为Spring WebClient带来了Windows NTLM认证的能力。这种方法遵循了NTLM的挑战-响应协议,并允许开发者在响应式应用中与NTLM保护的资源进行交互。虽然实现过程比简单的Basic认证复杂,但其提供了高度的灵活性和控制力。在实际应用中,务必注意凭据的安全管理、lmCompatibility的正确配置以及健壮的错误处理。对于利用当前用户上下文进行认证的特殊需求,则需要考虑更深层次的系统集成方案。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

837

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

741

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

736

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.8万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.0万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号