杰瑞科技汇

Java Windows验证如何实现?

Java 实现这一功能主要有两种场景:

Java Windows验证如何实现?-图1
(图片来源网络,侵删)
  1. 客户端验证:Java 应用程序作为客户端,去访问需要 Windows 身份验证的资源(如 HTTP 资源、网络共享文件夹)。
  2. 服务器端验证:Java 应用程序作为服务器(如 Tomcat),接收来自 Windows 客户端的请求,并验证其提供的 Windows 凭据。

下面我们分别对这两种场景进行详细说明。


客户端验证(Java 应用访问 Windows 资源)

这是最常见的需求,你的 Java 程序需要访问一个配置了 Windows 身份验证的 IIS 网站,或者访问一个网络共享盘。

访问配置了 Windows 身份验证的 HTTP/HTTPS 资源

当使用 HttpURLConnection 或第三方库(如 Apache HttpClient, OkHttp)访问一个 IIS 网站时,如果该站点配置了“集成 Windows 身份验证”,标准的 HTTP 请求会收到一个 401 Unauthorized 响应,其中包含一个 WWW-Authenticate 头,提示使用 NegotiateNTLM 协议。

关键点:

Java Windows验证如何实现?-图2
(图片来源网络,侵删)
  • Negotiate 协议:这是首选,它会自动在 Kerberos 和 NTLM 之间选择,如果客户端和服务器在同一个 Active Directory (AD) 域内,并且配置正确,它会使用更安全、性能更好的 Kerberos。
  • NTLM 协议:一种较旧的挑战-响应协议,不依赖于域控制器,但在跨平台和非域环境下兼容性更好。

实现方式:

由于 Java 标准库不直接支持 NTLM/Kerberos,通常需要借助第三方库。

示例:使用 Apache HttpClient (推荐)

HttpClient 对 NTLM 和 SPNEGO (Kerberos 的上层协议) 有很好的支持。

Java Windows验证如何实现?-图3
(图片来源网络,侵删)

添加 Maven 依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version> <!-- 使用较新稳定版本 -->
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient-cache</artifactId>
    <version>4.5.14</version>
</dependency>

编写 Java 代码:

import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class WindowsAuthHttpClient {
    public static void main(String[] args) {
        // 目标 URL,配置了 Windows 身份验证的 IIS 网站
        String url = "http://your-windows-server/protected-site";
        // --- 关键配置 ---
        // 1. 当前登录 Windows 的用户名
        String username = System.getProperty("user.name");
        // 2. 当前登录 Windows 的域名
        // 如果是本机账户,域名通常是计算机名,如果是域账户,是域名。
        // 你也可以手动指定,如 "YOURDOMAIN"
        String domain = System.getenv("USERDOMAIN"); // 从环境变量获取
        // 3. 密码 (通常为 null,让 JVM 使用当前登录用户的凭据)
        String password = null;
        // 创建凭证提供者
        BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
        // NTCredentials 用于 NTLM 认证
        credsProvider.setCredentials(
                new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
                new NTCredentials(username, password, "", domain));
        // 创建 HttpClient,并设置凭证提供者
        try (CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultCredentialsProvider(credsProvider)
                .build()) {
            HttpGet httpGet = new HttpGet(url);
            System.out.println("Executing request " + httpGet.getRequestLine() + " to " + url);
            HttpResponse response = httpClient.execute(httpGet);
            System.out.println("Response status: " + response.getStatusLine());
            if (response.getEntity() != null) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("Response body (first 200 chars): " + responseBody.substring(0, Math.min(200, responseBody.length())));
            }
            EntityUtils.consume(response.getEntity());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代码解释:

  • System.getProperty("user.name")System.getenv("USERDOMAIN") 是获取当前 Windows 用户信息的标准方法。
  • NTCredentialsHttpClient 中用于 NTLM 认证的凭证类,密码设为 null 是关键,这会指示 JVM 使用当前登录用户的 安全上下文(Security Context),即用户登录时输入的密码,无需在代码中硬编码。
  • HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build() 创建了一个会自动使用这些凭证的 HttpClient。

访问网络共享文件夹 (SMB)

Java 访问网络共享文件夹通常使用 JCIFS 或 SmbJ 库,SmbJ 是目前更现代和活跃的选择。

添加 Maven 依赖 (SmbJ):

<dependency>
    <groupId>com.hierynomus</groupId>
    <artifactId>smbj</artifactId>
    <version>0.11.5</version> <!-- 使用较新稳定版本 -->
</dependency>

编写 Java 代码:

import com.hierynomus.smbj.SMBClient;
import com.hierynomus.smbj.auth.AuthenticationContext;
import com.hierynomus.smbj.connection.Connection;
import com.hierynomus.smbj.session.Session;
import com.hierynomus.smbj.share.DiskShare;
import com.hierynomus.smbj.SMBException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
public class WindowsAuthSMB {
    public static void main(String[] args) {
        String server = "your-file-server"; // 文件服务器 IP 或主机名
        String shareName = "SharedFolder"; // 共享文件夹名
        String remotePath = "test.txt"; // 要访问的文件
        String domain = System.getenv("USERDOMAIN");
        String username = System.getProperty("user.name");
        SMBClient client = new SMBClient();
        try (Connection connection = client.connect(server)) {
            if (connection == null) {
                throw new RuntimeException("Failed to connect to " + server);
            }
            // 使用当前登录用户的凭据进行认证
            AuthenticationContext ac = new AuthenticationContext(username, null, domain);
            try (Session session = connection.authenticate(ac)) {
                System.out.println("Authentication successful.");
                try (DiskShare share = (DiskShare) session.connectShare(shareName)) {
                    System.out.println("Connected to share: " + shareName);
                    // 现在可以进行文件操作,如 list, exists, openFile 等
                    boolean fileExists = share.fileExists(remotePath);
                    System.out.println("File " + remotePath + " exists: " + fileExists);
                }
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException | SMBException e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
    }
}

代码解释:

  • 和 HttpClient 一样,AuthenticationContext 的密码也设为 null,让 SmbJ 使用当前用户的登录凭据。
  • 这需要 Java 进程以用户的身份运行,而不是作为服务运行(除非服务配置了“允许服务与桌面交互”并登录了特定用户,但这不推荐)。

服务器端验证(Java 应用接收 Windows 认证)

当你的 Java Web 应用(如运行在 Tomcat 上的应用)需要接收来自 Windows 客户端的请求,并验证这些客户端的 Windows 身份时,你需要配置 Web 服务器(如 Tomcat)来处理这个流程。

核心流程:

  1. 客户端(如 Windows 上的 IE/Edge)访问你的 Java Web 应用。
  2. Tomcat(或其前端的反向代理如 IIS/Apache)收到请求,并要求客户端进行 Windows 身份验证。
  3. 客户端将其 Windows 凭据(通过 NTLM/Kerberos 协议)发送给服务器。
  4. 服务器(Tomcat)将这些凭据转发给你的 Java 应用。
  5. Java 应用需要解析这些凭据,并向 Windows 域控制器进行验证。

实现方式:

这通常需要一个 Servlet 过滤器来拦截请求,并处理身份验证,一个强大的库是 Spring Security,它极大地简化了这个过程。

示例:使用 Spring Security 实现

项目配置 (Spring Boot + Spring Security): 创建一个 Spring Boot 项目,并添加 spring-boot-starter-security 依赖。

配置 Spring Security: 你需要配置一个 SecurityFilterChain 来启用 Windows 身份验证,Spring Security 内置了对 RequestHeaderAuthenticationFilter 的支持,该过滤器可以从特定的 HTTP 头中提取用户信息。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 1. 关闭 CSRF,因为通常这些是内部服务到服务的调用
        http.csrf().disable();
        // 2. 配置 URL 权限
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll() // 公开访问的路径
            .anyRequest().authenticated(); // 其他所有路径都需要认证
        // 3. 添加 Windows 身份验证过滤器
        // 这个过滤器会从请求头中提取用户名
        http.addFilterBefore(new WindowsAuthenticationFilter(), BasicAuthenticationFilter.class);
    }
    // 如果你使用 Spring Security 5.6+,推荐使用这种方式
    /*
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http
            .csrf().disable()
            .authorizeRequests()
                .requestMatchers(mvcMatcherBuilder.antMatchers("/public/**")).permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(new WindowsAuthenticationFilter(), BasicAuthenticationFilter.class);
        return http.build();
    }
    */
}

自定义 Windows 认证过滤器: 这是核心逻辑,你需要从请求头中获取用户信息,并创建一个 Authentication 对象。

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class WindowsAuthenticationFilter extends BasicAuthenticationFilter {
    public WindowsAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 从请求头中获取用户信息
        // 头的名称取决于服务器配置,通常是 'LOGON_USER' 或 'AUTH_USER'
        String username = request.getHeader("LOGON_USER"); 
        if (username == null || username.isEmpty()) {
            // 如果没有头信息,说明没有进行 Windows 认证,继续过滤器链
            chain.doFilter(request, response);
            return;
        }
        // 去除域名前缀,如 "DOMAIN\\username" -> "username"
        String shortUsername = username.contains("\\") ? username.split("\\\\")[1] : username;
        // 创建一个简单的认证令牌
        // 在实际应用中,你可能需要更复杂的验证逻辑,比如联系域控制器
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 给予一个默认角色
        Authentication auth = new UsernamePasswordAuthenticationToken(shortUsername, null, authorities);
        // 设置认证详情
        AuthenticationDetailsSource<HttpServletRequest, ?> ads = this.getAuthenticationDetailsSource();
        auth.setDetails(ads.buildDetails(request));
        // 将认证信息存入 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(auth);
        chain.doFilter(request, response);
    }
}

在 Controller 中获取当前用户: 在你的 Controller 中,你可以轻松地获取到已认证的用户信息。

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
    @GetMapping("/user-info")
    public String getUserInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return "Hello, " + authentication.getName() + "!";
        }
        return "Not authenticated.";
    }
    @GetMapping("/admin")
    public String getAdminPage() {
        // 只有拥有 ROLE_ADMIN 角色的用户才能访问
        return "Admin Page";
    }
}

服务器配置 (Tomcat + IIS/Apache): 为了让 IIS 能将 LOGON_USER 头传递给 Tomcat,你需要配置 IIS 的“身份验证”和“URL 重写”模块,Tomcat 本身不直接处理 Windows 身份验证,通常需要一个前端 Web 服务器(如 IIS)来处理它,然后将已验证的用户信息通过代理头传递给 Tomcat。


重要注意事项和常见问题

  1. 运行环境

    • 客户端验证:Java 进程必须以目标 Windows 账户的用户身份运行,如果作为服务运行,需要配置服务以特定用户身份登录,并勾选“允许服务与桌面交互”(不推荐,有安全风险)。
    • 服务器端验证:前端服务器(IIS)必须正确配置,并信任客户端。
  2. Kerberos vs. NTLM

    • Kerberos:性能更好,安全性更高,是域环境下的首选,但配置复杂,需要 SPN(服务主体名称)的正确设置。
    • NTLM:配置简单,但性能较差,且存在已知的漏洞(如 Pass-the-Hash),仅在没有 Kerberos 选项时使用。
  3. 调试

    • 客户端:开启 HttpClient 的日志,可以看到完整的 NTLM/Kerberos 握手过程。
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
      // ...
      PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
      CloseableHttpClient httpClient = HttpClients.custom()
          .setConnectionManager(cm)
          .setCredentialsProvider(credsProvider)
          .build();
      // 然后使用这个 httpClient
    • 服务器端:检查 LOGON_USER 头是否存在,使用浏览器开发者工具查看网络请求的响应头。
  4. 跨域/跨信任:如果客户端和服务器不在同一个域或没有信任关系,Kerberos 认证会失败,此时可能需要回退到 NTLM 或手动配置信任关系。

  5. Java 版本:较新的 Java 版本对 Kerberos 的支持更好,在 java.security 文件中可以配置 Kerberos 的登录模块 (com.sun.security.module.JndiLoginModule)。

希望这份详细的指南能帮助你理解和实现 Java 中的 Windows 验证!

分享:
扫描分享到社交APP
上一篇
下一篇