From c7414c7c1e4745a8b8fc709dbfe93045383a7811 Mon Sep 17 00:00:00 2001 From: yuyantian <1184477297@qq.com> Date: Fri, 6 Mar 2026 13:58:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=9F=9F=E6=8E=A7=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/hxhq/HxhqIntegrationApplication.java | 5 + .../main/java/com/hxhq/common/ad/ADProperties.java | 102 +++++++ .../java/com/hxhq/common/ad/JdkADAuthUtil.java | 298 +++++++++++++++++++++ .../java/com/hxhq/controller/ADAuthController.java | 44 +++ .../src/main/resources/bootstrap.yml | 22 +- 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/ADProperties.java create mode 100644 hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/JdkADAuthUtil.java create mode 100644 hxhq-modules/hxhq-integration/src/main/java/com/hxhq/controller/ADAuthController.java diff --git a/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/HxhqIntegrationApplication.java b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/HxhqIntegrationApplication.java index 555dbcb..48bd673 100644 --- a/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/HxhqIntegrationApplication.java +++ b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/HxhqIntegrationApplication.java @@ -1,9 +1,14 @@ package com.hxhq; +import com.hxhq.common.ad.ADProperties; +import com.hxhq.common.ad.JdkADAuthUtil; +import com.hxhq.common.core.web.domain.AjaxResult; import com.hxhq.common.security.annotation.EnableCustomConfig; import com.hxhq.common.security.annotation.EnableRyFeignClients; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ComponentScan; /** diff --git a/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/ADProperties.java b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/ADProperties.java new file mode 100644 index 0000000..9ffc796 --- /dev/null +++ b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/ADProperties.java @@ -0,0 +1,102 @@ +package com.hxhq.common.ad; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author yuyantian + * @date 2026/2/11 + * @desc + */ +/** + * AD域控配置类(绑定application.yml中的hxhq.ad配置) + */ +@Component +@ConfigurationProperties(prefix = "hxhq.ad") +public class ADProperties { + // 服务器地址 + private String serverHost; + // 端口 + private int serverPort = 389; + // 基础DN + private String baseDn; + // 是否启用SSL + private boolean useSsl = false; + // 连接超时 + private int connectTimeout = 5000; + // 只读账号子配置 + private ReadOnly readOnly = new ReadOnly(); + + // 内部类:只读账号配置 + public static class ReadOnly { + private String username; + private String password; + + // Getter & Setter + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } + + // Getter & Setter + public String getServerHost() { + return serverHost; + } + + public void setServerHost(String serverHost) { + this.serverHost = serverHost; + } + + public int getServerPort() { + return serverPort; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public String getBaseDn() { + return baseDn; + } + + public void setBaseDn(String baseDn) { + this.baseDn = baseDn; + } + + public boolean isUseSsl() { + return useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public ReadOnly getReadOnly() { + return readOnly; + } + + public void setReadOnly(ReadOnly readOnly) { + this.readOnly = readOnly; + + } +} \ No newline at end of file diff --git a/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/JdkADAuthUtil.java b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/JdkADAuthUtil.java new file mode 100644 index 0000000..7dd2ac6 --- /dev/null +++ b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/common/ad/JdkADAuthUtil.java @@ -0,0 +1,298 @@ +package com.hxhq.common.ad; + + +import com.hxhq.common.core.web.domain.AjaxResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.stereotype.Component; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import java.util.Hashtable; + +/** + * 纯JDK实现的AD域控核心接口(Spring Bean化) + * 核心功能:1. 账号密码鉴权 2. 验证账号是否存在(基于只读账号) + */ +@Component // 标记为Spring组件,自动注入 +public class JdkADAuthUtil { + private static final Logger log = LoggerFactory.getLogger(JdkADAuthUtil.class); + private final ADProperties adProperties; + // 只读账号(从配置文件注入) + private final String readOnlyUsername; + private final String readOnlyPassword; + + /** + * 构造器注入AD配置(Spring推荐方式) + */ + public JdkADAuthUtil(ADProperties adProperties) { + this.adProperties = adProperties; + this.readOnlyUsername = adProperties.getReadOnly().getUsername(); + this.readOnlyPassword = adProperties.getReadOnly().getPassword(); + } + + // ====================== 核心接口1:AD账号密码鉴权 ====================== + /** + * 验证AD账号密码是否正确(登录鉴权) + * + * @param username 纯用户名(如:ELNtest01) + * @param password 密码 + * @return true=鉴权成功,false=鉴权失败 + */ + public AjaxResult validateAccount(String username, String password) { + // 入参校验 + if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { + log.debug("用户名或密码为空,鉴权失败"); + return AjaxResult.error("用户名或密码为空,鉴权失败"); + } + + LdapContext ldapContext = null; + try { + // 构建LDAP连接环境(绑定用户账号密码) + Hashtable env = buildLDAPEnv(username, password); + log.debug("开始鉴权,用户主体名:{}", env.get(Context.SECURITY_PRINCIPAL)); + + // 尝试绑定(绑定成功=账号密码正确) + ldapContext = new InitialLdapContext(env, null); + log.debug("账号[{}]鉴权成功", username); + return AjaxResult.error("账号[{}]鉴权成功", username); + + } catch (NamingException e) { + String errorMsg = e.getMessage().toLowerCase(); + log.error("账号[{}]鉴权失败:{}", username, errorMsg); + // 解析常见失败原因 + if (errorMsg.contains("data 525")) { + log.error("鉴权失败原因:用户不存在"); + return AjaxResult.error("鉴权失败原因:用户不存在"); + } else if (errorMsg.contains("data 52e")) { + log.error("鉴权失败原因:密码错误"); + return AjaxResult.error("鉴权失败原因:密码错误"); + } else if (errorMsg.contains("data 532")) { + log.error("鉴权失败原因:密码过期"); + return AjaxResult.error("鉴权失败原因:密码过期"); + } else if (errorMsg.contains("data 533")) { + log.error("鉴权失败原因:账号禁用"); + return AjaxResult.error("鉴权失败原因:账号禁用"); + } else if (errorMsg.contains("data 775")) { + log.error("鉴权失败原因:账号锁定"); + return AjaxResult.error("鉴权失败原因:账号锁定"); + } + } finally { + closeLdapContext(ldapContext); + } + return AjaxResult.success("账号[{}]鉴权成功", username); + } + + // ====================== 核心接口2:验证AD账号是否存在 ====================== + /** + * 验证账号是否在AD域中存在(基于只读账号,无需用户密码) + * + * @param username 纯用户名(如:ELNtest01),支持带@后缀的用户名自动处理 + * @return true=账号存在,false=账号不存在/查询失败 + */ + public AjaxResult checkAccountExists(String username) { + // 入参校验 + if (username == null || username.trim().isEmpty()) { + log.debug("用户名不能为空,查询失败"); + return AjaxResult.error("用户名不能为空,查询失败"); + } + + // 处理用户名:如果带@则直接用,否则用sAMAccountName查询(核心规则) + String queryUsername = username; + if (username.contains("@")) { + // 带后缀,用userPrincipalName查询 + queryUsername = username; + } else { + // 纯用户名,用sAMAccountName查询(符合你的规则) + queryUsername = username; + } + + LdapContext ldapContext = null; + NamingEnumeration results = null; + try { + // 构建LDAP连接环境(强制使用只读账号) + Hashtable env = buildLDAPEnvForQuery(); + ldapContext = new InitialLdapContext(env, null); + + // 构建查询条件:根据是否带后缀选择查询字段 + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[]{"sAMAccountName", "userPrincipalName"}); + searchControls.setTimeLimit(3000); + String filter = username.contains("@") + ? String.format("(userPrincipalName=%s)", escapeLDAPFilter(queryUsername)) + : String.format("(sAMAccountName=%s)", escapeLDAPFilter(queryUsername)); + + // 执行查询 + results = ldapContext.search(adProperties.getBaseDn(), filter, searchControls); + boolean exists = results != null && results.hasMoreElements(); + + if (exists) { + log.debug("账号[{}]在AD域中存在", username); + return AjaxResult.success("账号[{}]在AD域中存在", username); + } else { + log.error("账号[{}]在AD域中不存在", username); + return AjaxResult.error("账号[{}]在AD域中不存在", username); + } + } catch (IllegalArgumentException e) { + log.error("查询失败:{}", e.getMessage()); + return AjaxResult.error("查询失败:{}", e.getMessage()); + } catch (NamingException e) { + log.error("查询账号[{}]是否存在失败:{}", username, e.getMessage()); + // 精准提示只读账号绑定失败 + if (e.getMessage().toLowerCase().contains("error code 49")) { + log.error("查询失败原因:只读账号绑定失败(账号/密码错误或权限不足)"); + return AjaxResult.error("查询失败原因:只读账号绑定失败(账号/密码错误或权限不足)"); + } + } finally { + closeNamingEnumeration(results); + closeLdapContext(ldapContext); + } + return AjaxResult.error("查询失败:查询账号[{}]是否存在失败", username); + } + + // ====================== 私有辅助方法 ====================== + /** + * 构建鉴权用的LDAP连接环境 + */ + private Hashtable buildLDAPEnv(String username, String password) { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + // 拼接LDAP连接地址 + String ldapUrl = adProperties.isUseSsl() + ? "ldaps://" + adProperties.getServerHost() + ":" + adProperties.getServerPort() + : "ldap://" + adProperties.getServerHost() + ":" + adProperties.getServerPort(); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + + // 处理用户名:带@直接用,否则拼接UPN + String userPrincipal = username; + if (!username.contains("@")) { + String domainSuffix = adProperties.getBaseDn().replaceAll("DC=", "").replace(",", "."); + userPrincipal = username + "@" + domainSuffix; + } + env.put(Context.SECURITY_PRINCIPAL, userPrincipal); + env.put(Context.SECURITY_CREDENTIALS, password); + + // 连接超时 + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(adProperties.getConnectTimeout())); + return env; + } + + /** + * 构建查询用的LDAP连接环境(只读账号) + */ + private Hashtable buildLDAPEnvForQuery() { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + // 拼接LDAP连接地址 + String ldapUrl = adProperties.isUseSsl() + ? "ldaps://" + adProperties.getServerHost() + ":" + adProperties.getServerPort() + : "ldap://" + adProperties.getServerHost() + ":" + adProperties.getServerPort(); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + + // 校验只读账号配置 + if (readOnlyUsername == null || readOnlyUsername.trim().isEmpty() + || readOnlyPassword == null || readOnlyPassword.trim().isEmpty()) { + throw new IllegalArgumentException("只读账号未配置,无法执行AD账号查询!"); + } + + // 拼接只读账号UPN + String domainSuffix = adProperties.getBaseDn().replaceAll("DC=", "").replace(",", "."); + String readOnlyUpn = readOnlyUsername + "@" + domainSuffix; + env.put(Context.SECURITY_PRINCIPAL, readOnlyUpn); + env.put(Context.SECURITY_CREDENTIALS, readOnlyPassword); + log.debug("使用只读账号构建查询环境:{}", readOnlyUpn); + + // 连接超时 + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(adProperties.getConnectTimeout())); + return env; + } + + /** + * 转义LDAP过滤器特殊字符 + */ + private String escapeLDAPFilter(String input) { + if (input == null || input.isEmpty()) { + return input; + } + return input.replace("\\", "\\5c") + .replace("(", "\\28") + .replace(")", "\\29") + .replace("*", "\\2a") + .replace("\0", "\\00"); + } + + /** + * 关闭LDAP连接 + */ + private void closeLdapContext(LdapContext ctx) { + if (ctx != null) { + try { + ctx.close(); + } catch (NamingException e) { + log.warn("关闭LDAP连接失败:{}", e.getMessage()); + } + } + } + + /** + * 关闭结果集 + */ + private void closeNamingEnumeration(NamingEnumeration ne) { + if (ne != null) { + try { + ne.close(); + } catch (NamingException e) { + log.warn("关闭LDAP结果集失败:{}", e.getMessage()); + } + } + } + + public static void main(String[] args) { +// +// // 1. 配置AD基础信息(替换为你的实际配置) +// ADProperties adConfig = new ADProperties(); +// adConfig.setServerHost("172.21.10.1"); +// adConfig.setServerPort(389); +// adConfig.setBaseDn("DC=glpcd,DC=com"); +// adConfig.setUseSsl(false); +// adConfig.setConnectTimeout(5000); +// adConfig.getReadOnly().setUsername("adcon"); +// adConfig.getReadOnly().setPassword("JYL@it323"); +// +// // 2. 初始化工具类(★核心★:传入只读账号,替换为你的实际只读账号/密码) +// JdkADAuthUtil adAuthUtil = new JdkADAuthUtil(adConfig); +// +// // 3. 测试账号 +// String existUsername = "ELNtest01"; // 存在的账号 +// String nonExistUsername = "test_not_exists"; // 不存在的账号 +// String testPassword = "GLPcd_28"; // 测试账号密码 +// +// // 4. 测试接口2:验证账号是否存在(无需用户密码) +// AjaxResult exists = adAuthUtil.checkAccountExists(existUsername); +// System.out.println("===== 验证账号[" + existUsername + "]是否存在:" + (exists.isSuccess() ? "存在" : "不存在") + " ====="); +// +// AjaxResult exists1 = adAuthUtil.checkAccountExists("ELNtest01@glpcd.com"); +// System.out.println("===== 验证账号[" + existUsername + "]是否存在:" + (exists1.isSuccess()? "存在" : "不存在") + " ====="); +// +// AjaxResult nonExists = adAuthUtil.checkAccountExists(nonExistUsername); +// System.out.println("===== 验证账号[" + nonExistUsername + "]是否存在:" + (nonExists.isSuccess() ? "存在" : "不存在") + " ====="); +// +// // 5. 测试接口1:账号密码鉴权 +// AjaxResult authSuccess = adAuthUtil.validateAccount(existUsername, testPassword); +// System.out.println("===== 账号[" + existUsername + "]鉴权结果:" + (authSuccess.isSuccess() ? "成功" : "失败") + " ====="); +// +// AjaxResult authFail = adAuthUtil.validateAccount(existUsername, "wrong_password"); +// System.out.println("===== 账号[" + existUsername + "]错误密码鉴权结果:" + (authFail.isSuccess() ? "成功" : "失败") + " ====="); + } +} \ No newline at end of file diff --git a/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/controller/ADAuthController.java b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/controller/ADAuthController.java new file mode 100644 index 0000000..5d2d034 --- /dev/null +++ b/hxhq-modules/hxhq-integration/src/main/java/com/hxhq/controller/ADAuthController.java @@ -0,0 +1,44 @@ +package com.hxhq.controller; + +import com.hxhq.common.ad.JdkADAuthUtil; +import com.hxhq.common.core.web.domain.AjaxResult; +import io.swagger.v3.oas.models.responses.ApiResponse; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * @author yuyantian + * @date 2026/2/11 + * @desc + */ +@RestController +@RequestMapping("/ad") // 接口统一前缀 +public class ADAuthController { + + + // 注入AD工具类(Spring Bean) + @Resource + private JdkADAuthUtil jdkADAuthUtil; + + /** + * 接口1:AD账号密码鉴权 + * 请求示例:POST /ad/validate?username=ELNtest01&password=GLPcd_28 + */ + @PostMapping("/validate") + public AjaxResult validateAccount( + @RequestParam String username, + @RequestParam String password + ) { + return jdkADAuthUtil.validateAccount(username, password); + } + + /** + * 接口2:验证AD账号是否存在 + * 请求示例:GET /ad/exists?username=ELNtest01 + */ + @GetMapping("/exists") + public AjaxResult checkAccountExists(@RequestParam String username) { + return jdkADAuthUtil.checkAccountExists(username); + } +} diff --git a/hxhq-modules/hxhq-integration/src/main/resources/bootstrap.yml b/hxhq-modules/hxhq-integration/src/main/resources/bootstrap.yml index f7bfedd..b580ce6 100644 --- a/hxhq-modules/hxhq-integration/src/main/resources/bootstrap.yml +++ b/hxhq-modules/hxhq-integration/src/main/resources/bootstrap.yml @@ -35,4 +35,24 @@ token: logging: level: - com.alibaba.cloud.nacos: DEBUG \ No newline at end of file + com.alibaba.cloud.nacos: DEBUG + + + +# AD域控配置(核心) +hxhq: + ad: + # AD服务器地址(IP/域名) + server-host: 172.21.10.1 + # LDAP端口(389=非SSL,636=SSL) + server-port: 389 + # 基础DN(域根) + base-dn: DC=glpcd,DC=com + # 是否启用SSL(ldaps协议) + use-ssl: false + # 连接超时时间(毫秒) + connect-timeout: 5000 + # 只读账号配置(查询账号是否存在用) + read-only: + username: adcon # 只读账号(纯用户名) + password: JYL@it323 # 只读账号密码 \ No newline at end of file