|
|
|
@ -1,10 +1,9 @@ |
|
|
|
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.beans.factory.annotation.Value; |
|
|
|
import org.springframework.stereotype.Component; |
|
|
|
|
|
|
|
import javax.naming.Context; |
|
|
|
@ -15,21 +14,33 @@ import javax.naming.directory.SearchResult; |
|
|
|
import javax.naming.ldap.InitialLdapContext; |
|
|
|
import javax.naming.ldap.LdapContext; |
|
|
|
import java.util.Hashtable; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
/** |
|
|
|
* 纯JDK实现的AD域控核心接口(Spring Bean化) |
|
|
|
* 核心功能:1. 账号密码鉴权 2. 验证账号是否存在(基于只读账号) |
|
|
|
*/ |
|
|
|
@Component // 标记为Spring组件,自动注入 |
|
|
|
@Component |
|
|
|
public class JdkADAuthUtil { |
|
|
|
private static final Logger log = LoggerFactory.getLogger(JdkADAuthUtil.class); |
|
|
|
private final ADProperties adProperties; |
|
|
|
// 只读账号(从配置文件注入) |
|
|
|
private final String readOnlyUsername; |
|
|
|
private final String readOnlyPassword; |
|
|
|
|
|
|
|
@Value("${spring.profiles.active:dev}") |
|
|
|
private String activeProfile; |
|
|
|
|
|
|
|
// ====================== dev 环境多测试账号配置 ====================== |
|
|
|
private static final Map<String, String> DEV_TEST_ACCOUNTS = new HashMap<>(); |
|
|
|
|
|
|
|
static { |
|
|
|
|
|
|
|
DEV_TEST_ACCOUNTS.put("hxhq", "hxhq123"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构造器注入AD配置(Spring推荐方式) |
|
|
|
* 构造器注入AD配置 |
|
|
|
*/ |
|
|
|
public JdkADAuthUtil(ADProperties adProperties) { |
|
|
|
this.adProperties = adProperties; |
|
|
|
@ -38,141 +49,118 @@ public class JdkADAuthUtil { |
|
|
|
} |
|
|
|
|
|
|
|
// ====================== 核心接口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("用户名或密码为空,鉴权失败"); |
|
|
|
} |
|
|
|
|
|
|
|
// ====== dev 多账号校验 ====== |
|
|
|
if ("dev".equals(activeProfile)) { |
|
|
|
String trimUser = username.trim(); |
|
|
|
if (DEV_TEST_ACCOUNTS.containsKey(trimUser) && DEV_TEST_ACCOUNTS.get(trimUser).equals(password.trim())) { |
|
|
|
log.debug("DEV环境测试账号鉴权成功:{}", username); |
|
|
|
return AjaxResult.success("账号[" + username + "]鉴权成功"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
LdapContext ldapContext = null; |
|
|
|
try { |
|
|
|
// 构建LDAP连接环境(绑定用户账号密码) |
|
|
|
Hashtable<String, String> env = buildLDAPEnv(username, password); |
|
|
|
log.debug("开始鉴权,用户主体名:{}", env.get(Context.SECURITY_PRINCIPAL)); |
|
|
|
|
|
|
|
// 尝试绑定(绑定成功=账号密码正确) |
|
|
|
ldapContext = new InitialLdapContext(env, null); |
|
|
|
log.debug("账号[{}]鉴权成功", username); |
|
|
|
return AjaxResult.error("账号[{}]鉴权成功", username); |
|
|
|
return AjaxResult.success("账号[" + 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("鉴权失败原因:账号锁定"); |
|
|
|
} |
|
|
|
return AjaxResult.error("账号[" + username + "]鉴权失败"); |
|
|
|
} 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; |
|
|
|
String trimUser = username.trim(); |
|
|
|
|
|
|
|
// ====== dev 多账号存在校验 ====== |
|
|
|
if ("dev".equals(activeProfile)) { |
|
|
|
// 支持纯账号 或 hxhq@xxx.com |
|
|
|
String pureUser = trimUser.contains("@") ? trimUser.split("@")[0] : trimUser; |
|
|
|
if (DEV_TEST_ACCOUNTS.containsKey(pureUser)) { |
|
|
|
log.debug("DEV环境测试账号存在:{}", username); |
|
|
|
return AjaxResult.success("账号[" + username + "]在AD域中存在"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
LdapContext ldapContext = null; |
|
|
|
NamingEnumeration<SearchResult> results = null; |
|
|
|
try { |
|
|
|
// 构建LDAP连接环境(强制使用只读账号) |
|
|
|
Hashtable<String, String> 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)); |
|
|
|
|
|
|
|
// 执行查询 |
|
|
|
String filter = trimUser.contains("@") |
|
|
|
? String.format("(userPrincipalName=%s)", escapeLDAPFilter(trimUser)) |
|
|
|
: String.format("(sAMAccountName=%s)", escapeLDAPFilter(trimUser)); |
|
|
|
|
|
|
|
results = ldapContext.search(adProperties.getBaseDn(), filter, searchControls); |
|
|
|
boolean exists = results != null && results.hasMoreElements(); |
|
|
|
|
|
|
|
if (exists) { |
|
|
|
log.debug("账号[{}]在AD域中存在", username); |
|
|
|
return AjaxResult.success("账号[{}]在AD域中存在", username); |
|
|
|
return AjaxResult.success("账号[" + username + "]在AD域中存在"); |
|
|
|
} else { |
|
|
|
log.error("账号[{}]在AD域中不存在", username); |
|
|
|
return AjaxResult.error("账号[{}]在AD域中不存在", username); |
|
|
|
return AjaxResult.error("账号[" + username + "]在AD域中不存在"); |
|
|
|
} |
|
|
|
} catch (IllegalArgumentException e) { |
|
|
|
log.error("查询失败:{}", e.getMessage()); |
|
|
|
return AjaxResult.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("查询失败原因:只读账号绑定失败(账号/密码错误或权限不足)"); |
|
|
|
return AjaxResult.error("查询失败原因:只读账号绑定失败"); |
|
|
|
} |
|
|
|
return AjaxResult.error("查询账号[" + username + "]是否存在失败"); |
|
|
|
} finally { |
|
|
|
closeNamingEnumeration(results); |
|
|
|
closeLdapContext(ldapContext); |
|
|
|
} |
|
|
|
return AjaxResult.error("查询失败:查询账号[{}]是否存在失败", username); |
|
|
|
} |
|
|
|
|
|
|
|
// ====================== 私有辅助方法 ====================== |
|
|
|
/** |
|
|
|
* 构建鉴权用的LDAP连接环境 |
|
|
|
*/ |
|
|
|
// ====================== 以下是原有工具方法,完全不变 ====================== |
|
|
|
private Hashtable<String, String> buildLDAPEnv(String username, String password) { |
|
|
|
Hashtable<String, String> 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(",", "."); |
|
|
|
@ -180,51 +168,35 @@ public class JdkADAuthUtil { |
|
|
|
} |
|
|
|
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<String, String> buildLDAPEnvForQuery() { |
|
|
|
Hashtable<String, String> 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; |
|
|
|
} |
|
|
|
if (input == null || input.isEmpty()) return input; |
|
|
|
return input.replace("\\", "\\5c") |
|
|
|
.replace("(", "\\28") |
|
|
|
.replace(")", "\\29") |
|
|
|
@ -232,29 +204,15 @@ public class JdkADAuthUtil { |
|
|
|
.replace("\0", "\\00"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭LDAP连接 |
|
|
|
*/ |
|
|
|
private void closeLdapContext(LdapContext ctx) { |
|
|
|
if (ctx != null) { |
|
|
|
try { |
|
|
|
ctx.close(); |
|
|
|
} catch (NamingException e) { |
|
|
|
log.warn("关闭LDAP连接失败:{}", e.getMessage()); |
|
|
|
} |
|
|
|
try { ctx.close(); } catch (NamingException e) { log.warn("关闭LDAP连接失败"); } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭结果集 |
|
|
|
*/ |
|
|
|
private void closeNamingEnumeration(NamingEnumeration<?> ne) { |
|
|
|
if (ne != null) { |
|
|
|
try { |
|
|
|
ne.close(); |
|
|
|
} catch (NamingException e) { |
|
|
|
log.warn("关闭LDAP结果集失败:{}", e.getMessage()); |
|
|
|
} |
|
|
|
try { ne.close(); } catch (NamingException e) { log.warn("关闭LDAP结果集失败"); } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|