|
|
|
@ -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<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); |
|
|
|
|
|
|
|
} 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<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)); |
|
|
|
|
|
|
|
// 执行查询 |
|
|
|
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<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(",", "."); |
|
|
|
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<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; |
|
|
|
} |
|
|
|
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() ? "成功" : "失败") + " ====="); |
|
|
|
} |
|
|
|
} |