From a1005e91c830b913a7ae10b35b1ba34bae1605a7 Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 3 Sep 2025 13:30:13 +0800 Subject: [PATCH 1/5] add support of Windows AD Server --- .env.example | 20 ++- config/config.example.js | 11 +- src/services/ldapService.js | 343 ++++++++++++++++++++++++++++++++---- 3 files changed, 332 insertions(+), 42 deletions(-) diff --git a/.env.example b/.env.example index 62f7fcfb..b4866560 100644 --- a/.env.example +++ b/.env.example @@ -64,11 +64,16 @@ TRUST_PROXY=true # 🔐 LDAP 认证配置 LDAP_ENABLED=false +# 服务器类型:openldap 或 activedirectory +LDAP_SERVER_TYPE=openldap +# LDAP 服务器配置 LDAP_URL=ldaps://ldap-1.test1.bj.yxops.net:636 LDAP_BIND_DN=cn=admin,dc=example,dc=com LDAP_BIND_PASSWORD=admin_password LDAP_SEARCH_BASE=dc=example,dc=com +# 搜索过滤器 (OpenLDAP 使用 uid,AD 会自动使用 sAMAccountName/userPrincipalName) LDAP_SEARCH_FILTER=(uid={{username}}) +# 搜索属性 (根据服务器类型自动设置,也可手动指定) LDAP_SEARCH_ATTRIBUTES=dn,uid,cn,mail,givenName,sn LDAP_TIMEOUT=5000 LDAP_CONNECT_TIMEOUT=10000 @@ -85,13 +90,26 @@ LDAP_TLS_REJECT_UNAUTHORIZED=true # 服务器名称 (可选,用于 SNI) # LDAP_TLS_SERVERNAME=ldap.example.com -# 🗺️ LDAP 用户属性映射 +# 🗺️ LDAP 用户属性映射 (根据服务器类型自动设置默认值) LDAP_USER_ATTR_USERNAME=uid LDAP_USER_ATTR_DISPLAY_NAME=cn LDAP_USER_ATTR_EMAIL=mail LDAP_USER_ATTR_FIRST_NAME=givenName LDAP_USER_ATTR_LAST_NAME=sn +# 🏢 Windows Active Directory 示例配置 +# LDAP_SERVER_TYPE=activedirectory +# LDAP_URL=ldaps://ad-server.company.com:636 +# # 或使用全局目录端口进行森林范围搜索 +# LDAP_URL=ldap://ad-server.company.com:3268 +# LDAP_BIND_DN=CN=Service Account,CN=Users,DC=company,DC=com +# LDAP_BIND_PASSWORD=service_account_password +# LDAP_SEARCH_BASE=DC=company,DC=com +# # AD 用户属性映射 (可选,会自动使用 AD 默认值) +# LDAP_USER_ATTR_USERNAME=sAMAccountName +# LDAP_USER_ATTR_DISPLAY_NAME=displayName +# LDAP_USER_ATTR_EMAIL=mail + # 👥 用户管理配置 USER_MANAGEMENT_ENABLED=false DEFAULT_USER_ROLE=user diff --git a/config/config.example.js b/config/config.example.js index 433ecd1f..88074000 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -130,14 +130,20 @@ const config = { // 🔐 LDAP 认证配置 ldap: { enabled: process.env.LDAP_ENABLED === 'true', + // 服务器类型:'openldap' 或 'activedirectory' + serverType: process.env.LDAP_SERVER_TYPE || 'openldap', server: { url: process.env.LDAP_URL || 'ldap://localhost:389', bindDN: process.env.LDAP_BIND_DN || 'cn=admin,dc=example,dc=com', bindCredentials: process.env.LDAP_BIND_PASSWORD || 'admin', searchBase: process.env.LDAP_SEARCH_BASE || 'dc=example,dc=com', + // 搜索过滤器 - OpenLDAP 默认使用 uid,Windows AD 会自动使用 sAMAccountName/userPrincipalName searchFilter: process.env.LDAP_SEARCH_FILTER || '(uid={{username}})', + // 搜索属性 - 根据服务器类型自动设置默认值 searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES ? process.env.LDAP_SEARCH_ATTRIBUTES.split(',') + : process.env.LDAP_SERVER_TYPE === 'activedirectory' + ? ['dn', 'sAMAccountName', 'userPrincipalName', 'cn', 'displayName', 'mail', 'givenName', 'sn', 'memberOf', 'objectClass', 'userAccountControl'] : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000, connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000, @@ -161,9 +167,10 @@ const config = { servername: process.env.LDAP_TLS_SERVERNAME || undefined } }, + // 用户属性映射 - 根据服务器类型自动设置默认值 userMapping: { - username: process.env.LDAP_USER_ATTR_USERNAME || 'uid', - displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || 'cn', + username: process.env.LDAP_USER_ATTR_USERNAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), + displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), email: process.env.LDAP_USER_ATTR_EMAIL || 'mail', firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName', lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn' diff --git a/src/services/ldapService.js b/src/services/ldapService.js index 75b4e704..1cfb3ba5 100644 --- a/src/services/ldapService.js +++ b/src/services/ldapService.js @@ -8,6 +8,10 @@ class LdapService { this.config = config.ldap || {} this.client = null + // 设置服务器类型,默认为 OpenLDAP + this.serverType = this.config.serverType || 'openldap' + this.isActiveDirectory = this.serverType === 'activedirectory' + // 验证配置 - 只有在 LDAP 配置存在且启用时才验证 if (this.config && this.config.enabled) { this.validateConfiguration() @@ -54,6 +58,93 @@ class LdapService { } } + // 🔍 解析Windows AD用户名格式 + parseActiveDirectoryUsername(username) { + if (!this.isActiveDirectory) { + return { username, domain: null, format: 'simple' } + } + + const trimmedUsername = username.trim() + + // 检查UPN格式 (user@domain.com) + if (trimmedUsername.includes('@')) { + const parts = trimmedUsername.split('@') + if (parts.length === 2 && parts[0] && parts[1]) { + return { + username: parts[0], + domain: parts[1], + format: 'upn', + fullUsername: trimmedUsername + } + } + } + + // 检查域\用户名格式 (DOMAIN\user) + if (trimmedUsername.includes('\\')) { + const parts = trimmedUsername.split('\\') + if (parts.length === 2 && parts[0] && parts[1]) { + return { + username: parts[1], + domain: parts[0], + format: 'domain', + fullUsername: trimmedUsername + } + } + } + + // 简单用户名格式 + return { + username: trimmedUsername, + domain: null, + format: 'simple', + fullUsername: trimmedUsername + } + } + + // 🔍 获取服务器类型特定的搜索过滤器 + getServerSpecificSearchFilter(usernameInfo) { + if (this.isActiveDirectory) { + const { username, fullUsername } = usernameInfo + // Windows AD: 支持 sAMAccountName 和 userPrincipalName + if (fullUsername && fullUsername.includes('@')) { + // 如果是UPN格式,优先使用userPrincipalName搜索 + return `(|(userPrincipalName=${fullUsername})(sAMAccountName=${username}))` + } else { + // 否则同时搜索两个属性 + return `(|(sAMAccountName=${username})(userPrincipalName=${username}))` + } + } else { + // OpenLDAP: 使用配置的搜索过滤器或默认的uid + const filterTemplate = this.config.server.searchFilter || '(uid={{username}})' + return filterTemplate.replace('{{username}}', usernameInfo.username) + } + } + + // 🔍 获取服务器类型特定的搜索属性 + getServerSpecificSearchAttributes() { + if (this.isActiveDirectory) { + // Windows AD 特定属性 + return ( + this.config.server.searchAttributes || [ + 'dn', + 'sAMAccountName', + 'userPrincipalName', + 'cn', + 'displayName', + 'mail', + 'givenName', + 'sn', + 'memberOf', + 'objectClass', + 'userAccountControl' + ] + ) + } else { + // OpenLDAP 默认属性 + return this.config.server.searchAttributes || ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'] + } + } + // 🔍 提取LDAP条目的DN extractDN(ldapEntry) { if (!ldapEntry) { @@ -219,9 +310,12 @@ class LdapService { // 🔍 搜索用户 async searchUser(client, username) { return new Promise((resolve, reject) => { + // 解析用户名(对Windows AD进行特殊处理) + const usernameInfo = this.parseActiveDirectoryUsername(username) + // 防止LDAP注入:转义特殊字符 // 根据RFC 4515,需要转义的特殊字符:* ( ) \ NUL - const escapedUsername = username + const escapedUsername = usernameInfo.username .replace(/\\/g, '\\5c') // 反斜杠必须先转义 .replace(/\*/g, '\\2a') // 星号 .replace(/\(/g, '\\28') // 左括号 @@ -229,14 +323,41 @@ class LdapService { .replace(/\0/g, '\\00') // NUL字符 .replace(/\//g, '\\2f') // 斜杠 - const searchFilter = this.config.server.searchFilter.replace('{{username}}', escapedUsername) + // 如果是UPN格式,也需要转义完整用户名 + let escapedFullUsername = usernameInfo.fullUsername + if (escapedFullUsername && escapedFullUsername !== usernameInfo.username) { + escapedFullUsername = escapedFullUsername + .replace(/\\/g, '\\5c') + .replace(/\*/g, '\\2a') + .replace(/\(/g, '\\28') + .replace(/\)/g, '\\29') + .replace(/\0/g, '\\00') + .replace(/\//g, '\\2f') + } + + // 构建转义后的用户名信息 + const escapedUsernameInfo = { + ...usernameInfo, + username: escapedUsername, + fullUsername: escapedFullUsername + } + + // 获取服务器特定的搜索过滤器和属性 + const searchFilter = this.getServerSpecificSearchFilter(escapedUsernameInfo) + const searchAttributes = this.getServerSpecificSearchAttributes() + const searchOptions = { scope: 'sub', filter: searchFilter, - attributes: this.config.server.searchAttributes + attributes: searchAttributes } - logger.debug(`🔍 Searching for user: ${username} with filter: ${searchFilter}`) + logger.debug( + `🔍 Searching for user: ${username} (${usernameInfo.format} format) with filter: ${searchFilter}` + ) + if (this.isActiveDirectory && usernameInfo.domain) { + logger.debug(`🏢 Domain detected: ${usernameInfo.domain}`) + } const entries = [] @@ -254,7 +375,8 @@ class LdapService { type: typeof entry.dn, entryType: typeof entry, hasAttributes: !!entry.attributes, - attributeCount: entry.attributes ? entry.attributes.length : 0 + attributeCount: entry.attributes ? entry.attributes.length : 0, + serverType: this.serverType }) entries.push(entry) }) @@ -270,7 +392,7 @@ class LdapService { res.on('end', (result) => { logger.debug( - `✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries` + `✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries (${this.serverType})` ) if (entries.length === 0) { @@ -282,14 +404,17 @@ class LdapService { entryType: typeof entries[0], entryConstructor: entries[0].constructor?.name, entryKeys: Object.keys(entries[0]), - entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500) + entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500), + serverType: this.serverType }) } if (entries.length === 1) { resolve(entries[0]) } else { - logger.warn(`⚠️ Multiple LDAP entries found for username: ${username}`) + logger.warn( + `⚠️ Multiple LDAP entries found for username: ${username} (${this.serverType})` + ) resolve(entries[0]) // 使用第一个结果 } } @@ -350,13 +475,70 @@ class LdapService { attrMap[name] = values.length === 1 ? values[0] : values }) - // 根据配置映射用户属性 - const mapping = this.config.userMapping + // 根据服务器类型和配置映射用户属性 + if (this.isActiveDirectory) { + // Windows AD 特定属性映射 + const mapping = this.config.userMapping || {} - userInfo.displayName = attrMap[mapping.displayName] || username - userInfo.email = attrMap[mapping.email] || '' - userInfo.firstName = attrMap[mapping.firstName] || '' - userInfo.lastName = attrMap[mapping.lastName] || '' + // 显示名称:优先使用displayName,其次cn + userInfo.displayName = + attrMap[mapping.displayName || 'displayName'] || + attrMap[mapping.displayName || 'cn'] || + attrMap['displayName'] || + attrMap['cn'] || + username + + // 邮箱 + userInfo.email = + attrMap[mapping.email || 'mail'] || + attrMap['mail'] || + attrMap['userPrincipalName'] || // UPN作为后备邮箱 + '' + + // 名字 + userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || '' + + // 姓氏 + userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || '' + + // Windows AD 特有信息 + userInfo.sAMAccountName = attrMap['sAMAccountName'] || username + userInfo.userPrincipalName = attrMap['userPrincipalName'] || '' + + // 检查用户账户是否被禁用 + const { userAccountControl } = attrMap + if (userAccountControl) { + // 检查 ADS_UF_ACCOUNTDISABLE 标志位 (0x02) + const isDisabled = (parseInt(userAccountControl) & 0x02) !== 0 + if (isDisabled) { + userInfo.accountDisabled = true + logger.warn(`⚠️ Windows AD account is disabled: ${username}`) + } + } + + logger.debug('📋 Extracted Windows AD user info:', { + username: userInfo.username, + displayName: userInfo.displayName, + email: userInfo.email, + sAMAccountName: userInfo.sAMAccountName, + userPrincipalName: userInfo.userPrincipalName, + accountDisabled: userInfo.accountDisabled || false + }) + } else { + // OpenLDAP 标准属性映射 + const mapping = this.config.userMapping || {} + + userInfo.displayName = attrMap[mapping.displayName || 'cn'] || attrMap['cn'] || username + userInfo.email = attrMap[mapping.email || 'mail'] || attrMap['mail'] || '' + userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || '' + userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || '' + + logger.debug('📋 Extracted OpenLDAP user info:', { + username: userInfo.username, + displayName: userInfo.displayName, + email: userInfo.email + }) + } // 如果没有displayName,尝试组合firstName和lastName if (!userInfo.displayName || userInfo.displayName === username) { @@ -365,12 +547,6 @@ class LdapService { } } - logger.debug('📋 Extracted user info:', { - username: userInfo.username, - displayName: userInfo.displayName, - email: userInfo.email - }) - return userInfo } catch (error) { logger.error('❌ Error extracting user info:', error) @@ -386,23 +562,87 @@ class LdapService { const trimmedUsername = username.trim() - // 用户名只能包含字母、数字、下划线和连字符 - const usernameRegex = /^[a-zA-Z0-9_-]+$/ - if (!usernameRegex.test(trimmedUsername)) { - throw new Error('Username can only contain letters, numbers, underscores, and hyphens') - } + if (this.isActiveDirectory) { + // Windows AD 用户名验证:支持 UPN 和 domain\username 格式 + // UPN 格式:user@domain.com + if (trimmedUsername.includes('@')) { + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + if (!emailRegex.test(trimmedUsername)) { + throw new Error('Invalid UPN format (user@domain.com)') + } - // 长度限制 (防止过长的输入) - if (trimmedUsername.length > 64) { - throw new Error('Username cannot exceed 64 characters') - } + if (trimmedUsername.length > 256) { + throw new Error('UPN cannot exceed 256 characters') + } - // 不能以连字符开头或结尾 - if (trimmedUsername.startsWith('-') || trimmedUsername.endsWith('-')) { - throw new Error('Username cannot start or end with a hyphen') - } + return trimmedUsername + } - return trimmedUsername + // Domain\username 格式 + if (trimmedUsername.includes('\\')) { + const parts = trimmedUsername.split('\\') + if (parts.length !== 2 || !parts[0] || !parts[1]) { + throw new Error('Invalid domain\\username format') + } + + const domain = parts[0] + const user = parts[1] + + // 验证域名(允许字母数字和连字符) + const domainRegex = /^[a-zA-Z0-9-]+$/ + if (!domainRegex.test(domain)) { + throw new Error('Domain name can only contain letters, numbers, and hyphens') + } + + // 验证用户名部分 + const userRegex = /^[a-zA-Z0-9._-]+$/ + if (!userRegex.test(user)) { + throw new Error( + 'Username can only contain letters, numbers, dots, underscores, and hyphens' + ) + } + + if (trimmedUsername.length > 256) { + throw new Error('Domain\\username cannot exceed 256 characters') + } + + return trimmedUsername + } + + // 简单用户名格式(sAMAccountName) + const samAccountRegex = /^[a-zA-Z0-9._-]+$/ + if (!samAccountRegex.test(trimmedUsername)) { + throw new Error( + 'sAMAccountName can only contain letters, numbers, dots, underscores, and hyphens' + ) + } + + // sAMAccountName 长度限制(AD 限制为 20 字符) + if (trimmedUsername.length > 20) { + throw new Error('sAMAccountName cannot exceed 20 characters') + } + + return trimmedUsername + } else { + // OpenLDAP 用户名验证(原有逻辑) + // 用户名只能包含字母、数字、下划线和连字符 + const usernameRegex = /^[a-zA-Z0-9_-]+$/ + if (!usernameRegex.test(trimmedUsername)) { + throw new Error('Username can only contain letters, numbers, underscores, and hyphens') + } + + // 长度限制 (防止过长的输入) + if (trimmedUsername.length > 64) { + throw new Error('Username cannot exceed 64 characters') + } + + // 不能以连字符开头或结尾 + if (trimmedUsername.startsWith('-') || trimmedUsername.endsWith('-')) { + throw new Error('Username cannot start or end with a hyphen') + } + + return trimmedUsername + } } // 🔐 主要的登录验证方法 @@ -488,10 +728,21 @@ class LdapService { // 5. 提取用户信息 const userInfo = this.extractUserInfo(ldapEntry, sanitizedUsername) - // 6. 创建或更新本地用户 + // 6. Windows AD 特定检查:验证账户是否被禁用 + if (this.isActiveDirectory && userInfo.accountDisabled) { + logger.security( + `🔒 Disabled Windows AD account login attempt: ${sanitizedUsername} from LDAP authentication` + ) + return { + success: false, + message: 'Your account has been disabled. Please contact administrator.' + } + } + + // 7. 创建或更新本地用户 const user = await userService.createOrUpdateUser(userInfo) - // 7. 检查用户是否被禁用 + // 8. 检查用户是否被禁用 if (!user.isActive) { logger.security( `🔒 Disabled user LDAP login attempt: ${sanitizedUsername} from LDAP authentication` @@ -502,13 +753,15 @@ class LdapService { } } - // 8. 记录登录 + // 9. 记录登录 await userService.recordUserLogin(user.id) - // 9. 创建用户会话 + // 10. 创建用户会话 const sessionToken = await userService.createUserSession(user.id) - logger.info(`✅ LDAP authentication successful for user: ${sanitizedUsername}`) + logger.info( + `✅ LDAP authentication successful for user: ${sanitizedUsername} (${this.serverType})` + ) return { success: true, @@ -598,6 +851,8 @@ class LdapService { getConfigInfo() { const configInfo = { enabled: this.config.enabled, + serverType: this.serverType, + isActiveDirectory: this.isActiveDirectory, server: { url: this.config.server.url, searchBase: this.config.server.searchBase, @@ -619,6 +874,16 @@ class LdapService { } } + // Windows AD 特定配置信息 + if (this.isActiveDirectory) { + configInfo.activeDirectoryFeatures = { + supportsUPN: true, + supportsDomainUsername: true, + supportsGlobalCatalog: true, + checksAccountDisabled: true + } + } + return configInfo } } From 088cf8401fa4f13fb0407bbe914ff58bbdcf65da Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 3 Sep 2025 13:35:47 +0800 Subject: [PATCH 2/5] fix prettier issue --- config/config.example.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 88074000..3302c1ff 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -143,8 +143,20 @@ const config = { searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES ? process.env.LDAP_SEARCH_ATTRIBUTES.split(',') : process.env.LDAP_SERVER_TYPE === 'activedirectory' - ? ['dn', 'sAMAccountName', 'userPrincipalName', 'cn', 'displayName', 'mail', 'givenName', 'sn', 'memberOf', 'objectClass', 'userAccountControl'] - : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], + ? [ + 'dn', + 'sAMAccountName', + 'userPrincipalName', + 'cn', + 'displayName', + 'mail', + 'givenName', + 'sn', + 'memberOf', + 'objectClass', + 'userAccountControl' + ] + : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000, connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000, // TLS/SSL 配置 @@ -169,8 +181,12 @@ const config = { }, // 用户属性映射 - 根据服务器类型自动设置默认值 userMapping: { - username: process.env.LDAP_USER_ATTR_USERNAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), - displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), + username: + process.env.LDAP_USER_ATTR_USERNAME || + (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), + displayName: + process.env.LDAP_USER_ATTR_DISPLAY_NAME || + (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), email: process.env.LDAP_USER_ATTR_EMAIL || 'mail', firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName', lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn' From 00395694713eb26d5cf0c518b3fb0cf9781aa38e Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 3 Sep 2025 15:02:38 +0800 Subject: [PATCH 3/5] Revert "fix prettier issue" This reverts commit 088cf8401fa4f13fb0407bbe914ff58bbdcf65da. --- config/config.example.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 3302c1ff..88074000 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -143,20 +143,8 @@ const config = { searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES ? process.env.LDAP_SEARCH_ATTRIBUTES.split(',') : process.env.LDAP_SERVER_TYPE === 'activedirectory' - ? [ - 'dn', - 'sAMAccountName', - 'userPrincipalName', - 'cn', - 'displayName', - 'mail', - 'givenName', - 'sn', - 'memberOf', - 'objectClass', - 'userAccountControl' - ] - : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], + ? ['dn', 'sAMAccountName', 'userPrincipalName', 'cn', 'displayName', 'mail', 'givenName', 'sn', 'memberOf', 'objectClass', 'userAccountControl'] + : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000, connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000, // TLS/SSL 配置 @@ -181,12 +169,8 @@ const config = { }, // 用户属性映射 - 根据服务器类型自动设置默认值 userMapping: { - username: - process.env.LDAP_USER_ATTR_USERNAME || - (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), - displayName: - process.env.LDAP_USER_ATTR_DISPLAY_NAME || - (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), + username: process.env.LDAP_USER_ATTR_USERNAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), + displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), email: process.env.LDAP_USER_ATTR_EMAIL || 'mail', firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName', lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn' From f69333f312a73e57f8d89987e3b4243ef95d1214 Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 3 Sep 2025 15:03:14 +0800 Subject: [PATCH 4/5] Revert "add support of Windows AD Server" This reverts commit a1005e91c830b913a7ae10b35b1ba34bae1605a7. --- .env.example | 20 +-- config/config.example.js | 11 +- src/services/ldapService.js | 347 +++++------------------------------- 3 files changed, 44 insertions(+), 334 deletions(-) diff --git a/.env.example b/.env.example index b4866560..62f7fcfb 100644 --- a/.env.example +++ b/.env.example @@ -64,16 +64,11 @@ TRUST_PROXY=true # 🔐 LDAP 认证配置 LDAP_ENABLED=false -# 服务器类型:openldap 或 activedirectory -LDAP_SERVER_TYPE=openldap -# LDAP 服务器配置 LDAP_URL=ldaps://ldap-1.test1.bj.yxops.net:636 LDAP_BIND_DN=cn=admin,dc=example,dc=com LDAP_BIND_PASSWORD=admin_password LDAP_SEARCH_BASE=dc=example,dc=com -# 搜索过滤器 (OpenLDAP 使用 uid,AD 会自动使用 sAMAccountName/userPrincipalName) LDAP_SEARCH_FILTER=(uid={{username}}) -# 搜索属性 (根据服务器类型自动设置,也可手动指定) LDAP_SEARCH_ATTRIBUTES=dn,uid,cn,mail,givenName,sn LDAP_TIMEOUT=5000 LDAP_CONNECT_TIMEOUT=10000 @@ -90,26 +85,13 @@ LDAP_TLS_REJECT_UNAUTHORIZED=true # 服务器名称 (可选,用于 SNI) # LDAP_TLS_SERVERNAME=ldap.example.com -# 🗺️ LDAP 用户属性映射 (根据服务器类型自动设置默认值) +# 🗺️ LDAP 用户属性映射 LDAP_USER_ATTR_USERNAME=uid LDAP_USER_ATTR_DISPLAY_NAME=cn LDAP_USER_ATTR_EMAIL=mail LDAP_USER_ATTR_FIRST_NAME=givenName LDAP_USER_ATTR_LAST_NAME=sn -# 🏢 Windows Active Directory 示例配置 -# LDAP_SERVER_TYPE=activedirectory -# LDAP_URL=ldaps://ad-server.company.com:636 -# # 或使用全局目录端口进行森林范围搜索 -# LDAP_URL=ldap://ad-server.company.com:3268 -# LDAP_BIND_DN=CN=Service Account,CN=Users,DC=company,DC=com -# LDAP_BIND_PASSWORD=service_account_password -# LDAP_SEARCH_BASE=DC=company,DC=com -# # AD 用户属性映射 (可选,会自动使用 AD 默认值) -# LDAP_USER_ATTR_USERNAME=sAMAccountName -# LDAP_USER_ATTR_DISPLAY_NAME=displayName -# LDAP_USER_ATTR_EMAIL=mail - # 👥 用户管理配置 USER_MANAGEMENT_ENABLED=false DEFAULT_USER_ROLE=user diff --git a/config/config.example.js b/config/config.example.js index 88074000..433ecd1f 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -130,20 +130,14 @@ const config = { // 🔐 LDAP 认证配置 ldap: { enabled: process.env.LDAP_ENABLED === 'true', - // 服务器类型:'openldap' 或 'activedirectory' - serverType: process.env.LDAP_SERVER_TYPE || 'openldap', server: { url: process.env.LDAP_URL || 'ldap://localhost:389', bindDN: process.env.LDAP_BIND_DN || 'cn=admin,dc=example,dc=com', bindCredentials: process.env.LDAP_BIND_PASSWORD || 'admin', searchBase: process.env.LDAP_SEARCH_BASE || 'dc=example,dc=com', - // 搜索过滤器 - OpenLDAP 默认使用 uid,Windows AD 会自动使用 sAMAccountName/userPrincipalName searchFilter: process.env.LDAP_SEARCH_FILTER || '(uid={{username}})', - // 搜索属性 - 根据服务器类型自动设置默认值 searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES ? process.env.LDAP_SEARCH_ATTRIBUTES.split(',') - : process.env.LDAP_SERVER_TYPE === 'activedirectory' - ? ['dn', 'sAMAccountName', 'userPrincipalName', 'cn', 'displayName', 'mail', 'givenName', 'sn', 'memberOf', 'objectClass', 'userAccountControl'] : ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'], timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000, connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000, @@ -167,10 +161,9 @@ const config = { servername: process.env.LDAP_TLS_SERVERNAME || undefined } }, - // 用户属性映射 - 根据服务器类型自动设置默认值 userMapping: { - username: process.env.LDAP_USER_ATTR_USERNAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'), - displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'), + username: process.env.LDAP_USER_ATTR_USERNAME || 'uid', + displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || 'cn', email: process.env.LDAP_USER_ATTR_EMAIL || 'mail', firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName', lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn' diff --git a/src/services/ldapService.js b/src/services/ldapService.js index 1cfb3ba5..75b4e704 100644 --- a/src/services/ldapService.js +++ b/src/services/ldapService.js @@ -8,10 +8,6 @@ class LdapService { this.config = config.ldap || {} this.client = null - // 设置服务器类型,默认为 OpenLDAP - this.serverType = this.config.serverType || 'openldap' - this.isActiveDirectory = this.serverType === 'activedirectory' - // 验证配置 - 只有在 LDAP 配置存在且启用时才验证 if (this.config && this.config.enabled) { this.validateConfiguration() @@ -58,93 +54,6 @@ class LdapService { } } - // 🔍 解析Windows AD用户名格式 - parseActiveDirectoryUsername(username) { - if (!this.isActiveDirectory) { - return { username, domain: null, format: 'simple' } - } - - const trimmedUsername = username.trim() - - // 检查UPN格式 (user@domain.com) - if (trimmedUsername.includes('@')) { - const parts = trimmedUsername.split('@') - if (parts.length === 2 && parts[0] && parts[1]) { - return { - username: parts[0], - domain: parts[1], - format: 'upn', - fullUsername: trimmedUsername - } - } - } - - // 检查域\用户名格式 (DOMAIN\user) - if (trimmedUsername.includes('\\')) { - const parts = trimmedUsername.split('\\') - if (parts.length === 2 && parts[0] && parts[1]) { - return { - username: parts[1], - domain: parts[0], - format: 'domain', - fullUsername: trimmedUsername - } - } - } - - // 简单用户名格式 - return { - username: trimmedUsername, - domain: null, - format: 'simple', - fullUsername: trimmedUsername - } - } - - // 🔍 获取服务器类型特定的搜索过滤器 - getServerSpecificSearchFilter(usernameInfo) { - if (this.isActiveDirectory) { - const { username, fullUsername } = usernameInfo - // Windows AD: 支持 sAMAccountName 和 userPrincipalName - if (fullUsername && fullUsername.includes('@')) { - // 如果是UPN格式,优先使用userPrincipalName搜索 - return `(|(userPrincipalName=${fullUsername})(sAMAccountName=${username}))` - } else { - // 否则同时搜索两个属性 - return `(|(sAMAccountName=${username})(userPrincipalName=${username}))` - } - } else { - // OpenLDAP: 使用配置的搜索过滤器或默认的uid - const filterTemplate = this.config.server.searchFilter || '(uid={{username}})' - return filterTemplate.replace('{{username}}', usernameInfo.username) - } - } - - // 🔍 获取服务器类型特定的搜索属性 - getServerSpecificSearchAttributes() { - if (this.isActiveDirectory) { - // Windows AD 特定属性 - return ( - this.config.server.searchAttributes || [ - 'dn', - 'sAMAccountName', - 'userPrincipalName', - 'cn', - 'displayName', - 'mail', - 'givenName', - 'sn', - 'memberOf', - 'objectClass', - 'userAccountControl' - ] - ) - } else { - // OpenLDAP 默认属性 - return this.config.server.searchAttributes || ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn'] - } - } - // 🔍 提取LDAP条目的DN extractDN(ldapEntry) { if (!ldapEntry) { @@ -310,12 +219,9 @@ class LdapService { // 🔍 搜索用户 async searchUser(client, username) { return new Promise((resolve, reject) => { - // 解析用户名(对Windows AD进行特殊处理) - const usernameInfo = this.parseActiveDirectoryUsername(username) - // 防止LDAP注入:转义特殊字符 // 根据RFC 4515,需要转义的特殊字符:* ( ) \ NUL - const escapedUsername = usernameInfo.username + const escapedUsername = username .replace(/\\/g, '\\5c') // 反斜杠必须先转义 .replace(/\*/g, '\\2a') // 星号 .replace(/\(/g, '\\28') // 左括号 @@ -323,41 +229,14 @@ class LdapService { .replace(/\0/g, '\\00') // NUL字符 .replace(/\//g, '\\2f') // 斜杠 - // 如果是UPN格式,也需要转义完整用户名 - let escapedFullUsername = usernameInfo.fullUsername - if (escapedFullUsername && escapedFullUsername !== usernameInfo.username) { - escapedFullUsername = escapedFullUsername - .replace(/\\/g, '\\5c') - .replace(/\*/g, '\\2a') - .replace(/\(/g, '\\28') - .replace(/\)/g, '\\29') - .replace(/\0/g, '\\00') - .replace(/\//g, '\\2f') - } - - // 构建转义后的用户名信息 - const escapedUsernameInfo = { - ...usernameInfo, - username: escapedUsername, - fullUsername: escapedFullUsername - } - - // 获取服务器特定的搜索过滤器和属性 - const searchFilter = this.getServerSpecificSearchFilter(escapedUsernameInfo) - const searchAttributes = this.getServerSpecificSearchAttributes() - + const searchFilter = this.config.server.searchFilter.replace('{{username}}', escapedUsername) const searchOptions = { scope: 'sub', filter: searchFilter, - attributes: searchAttributes + attributes: this.config.server.searchAttributes } - logger.debug( - `🔍 Searching for user: ${username} (${usernameInfo.format} format) with filter: ${searchFilter}` - ) - if (this.isActiveDirectory && usernameInfo.domain) { - logger.debug(`🏢 Domain detected: ${usernameInfo.domain}`) - } + logger.debug(`🔍 Searching for user: ${username} with filter: ${searchFilter}`) const entries = [] @@ -375,8 +254,7 @@ class LdapService { type: typeof entry.dn, entryType: typeof entry, hasAttributes: !!entry.attributes, - attributeCount: entry.attributes ? entry.attributes.length : 0, - serverType: this.serverType + attributeCount: entry.attributes ? entry.attributes.length : 0 }) entries.push(entry) }) @@ -392,7 +270,7 @@ class LdapService { res.on('end', (result) => { logger.debug( - `✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries (${this.serverType})` + `✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries` ) if (entries.length === 0) { @@ -404,17 +282,14 @@ class LdapService { entryType: typeof entries[0], entryConstructor: entries[0].constructor?.name, entryKeys: Object.keys(entries[0]), - entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500), - serverType: this.serverType + entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500) }) } if (entries.length === 1) { resolve(entries[0]) } else { - logger.warn( - `⚠️ Multiple LDAP entries found for username: ${username} (${this.serverType})` - ) + logger.warn(`⚠️ Multiple LDAP entries found for username: ${username}`) resolve(entries[0]) // 使用第一个结果 } } @@ -475,70 +350,13 @@ class LdapService { attrMap[name] = values.length === 1 ? values[0] : values }) - // 根据服务器类型和配置映射用户属性 - if (this.isActiveDirectory) { - // Windows AD 特定属性映射 - const mapping = this.config.userMapping || {} + // 根据配置映射用户属性 + const mapping = this.config.userMapping - // 显示名称:优先使用displayName,其次cn - userInfo.displayName = - attrMap[mapping.displayName || 'displayName'] || - attrMap[mapping.displayName || 'cn'] || - attrMap['displayName'] || - attrMap['cn'] || - username - - // 邮箱 - userInfo.email = - attrMap[mapping.email || 'mail'] || - attrMap['mail'] || - attrMap['userPrincipalName'] || // UPN作为后备邮箱 - '' - - // 名字 - userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || '' - - // 姓氏 - userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || '' - - // Windows AD 特有信息 - userInfo.sAMAccountName = attrMap['sAMAccountName'] || username - userInfo.userPrincipalName = attrMap['userPrincipalName'] || '' - - // 检查用户账户是否被禁用 - const { userAccountControl } = attrMap - if (userAccountControl) { - // 检查 ADS_UF_ACCOUNTDISABLE 标志位 (0x02) - const isDisabled = (parseInt(userAccountControl) & 0x02) !== 0 - if (isDisabled) { - userInfo.accountDisabled = true - logger.warn(`⚠️ Windows AD account is disabled: ${username}`) - } - } - - logger.debug('📋 Extracted Windows AD user info:', { - username: userInfo.username, - displayName: userInfo.displayName, - email: userInfo.email, - sAMAccountName: userInfo.sAMAccountName, - userPrincipalName: userInfo.userPrincipalName, - accountDisabled: userInfo.accountDisabled || false - }) - } else { - // OpenLDAP 标准属性映射 - const mapping = this.config.userMapping || {} - - userInfo.displayName = attrMap[mapping.displayName || 'cn'] || attrMap['cn'] || username - userInfo.email = attrMap[mapping.email || 'mail'] || attrMap['mail'] || '' - userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || '' - userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || '' - - logger.debug('📋 Extracted OpenLDAP user info:', { - username: userInfo.username, - displayName: userInfo.displayName, - email: userInfo.email - }) - } + userInfo.displayName = attrMap[mapping.displayName] || username + userInfo.email = attrMap[mapping.email] || '' + userInfo.firstName = attrMap[mapping.firstName] || '' + userInfo.lastName = attrMap[mapping.lastName] || '' // 如果没有displayName,尝试组合firstName和lastName if (!userInfo.displayName || userInfo.displayName === username) { @@ -547,6 +365,12 @@ class LdapService { } } + logger.debug('📋 Extracted user info:', { + username: userInfo.username, + displayName: userInfo.displayName, + email: userInfo.email + }) + return userInfo } catch (error) { logger.error('❌ Error extracting user info:', error) @@ -562,87 +386,23 @@ class LdapService { const trimmedUsername = username.trim() - if (this.isActiveDirectory) { - // Windows AD 用户名验证:支持 UPN 和 domain\username 格式 - // UPN 格式:user@domain.com - if (trimmedUsername.includes('@')) { - const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ - if (!emailRegex.test(trimmedUsername)) { - throw new Error('Invalid UPN format (user@domain.com)') - } - - if (trimmedUsername.length > 256) { - throw new Error('UPN cannot exceed 256 characters') - } - - return trimmedUsername - } - - // Domain\username 格式 - if (trimmedUsername.includes('\\')) { - const parts = trimmedUsername.split('\\') - if (parts.length !== 2 || !parts[0] || !parts[1]) { - throw new Error('Invalid domain\\username format') - } - - const domain = parts[0] - const user = parts[1] - - // 验证域名(允许字母数字和连字符) - const domainRegex = /^[a-zA-Z0-9-]+$/ - if (!domainRegex.test(domain)) { - throw new Error('Domain name can only contain letters, numbers, and hyphens') - } - - // 验证用户名部分 - const userRegex = /^[a-zA-Z0-9._-]+$/ - if (!userRegex.test(user)) { - throw new Error( - 'Username can only contain letters, numbers, dots, underscores, and hyphens' - ) - } - - if (trimmedUsername.length > 256) { - throw new Error('Domain\\username cannot exceed 256 characters') - } - - return trimmedUsername - } - - // 简单用户名格式(sAMAccountName) - const samAccountRegex = /^[a-zA-Z0-9._-]+$/ - if (!samAccountRegex.test(trimmedUsername)) { - throw new Error( - 'sAMAccountName can only contain letters, numbers, dots, underscores, and hyphens' - ) - } - - // sAMAccountName 长度限制(AD 限制为 20 字符) - if (trimmedUsername.length > 20) { - throw new Error('sAMAccountName cannot exceed 20 characters') - } - - return trimmedUsername - } else { - // OpenLDAP 用户名验证(原有逻辑) - // 用户名只能包含字母、数字、下划线和连字符 - const usernameRegex = /^[a-zA-Z0-9_-]+$/ - if (!usernameRegex.test(trimmedUsername)) { - throw new Error('Username can only contain letters, numbers, underscores, and hyphens') - } - - // 长度限制 (防止过长的输入) - if (trimmedUsername.length > 64) { - throw new Error('Username cannot exceed 64 characters') - } - - // 不能以连字符开头或结尾 - if (trimmedUsername.startsWith('-') || trimmedUsername.endsWith('-')) { - throw new Error('Username cannot start or end with a hyphen') - } - - return trimmedUsername + // 用户名只能包含字母、数字、下划线和连字符 + const usernameRegex = /^[a-zA-Z0-9_-]+$/ + if (!usernameRegex.test(trimmedUsername)) { + throw new Error('Username can only contain letters, numbers, underscores, and hyphens') } + + // 长度限制 (防止过长的输入) + if (trimmedUsername.length > 64) { + throw new Error('Username cannot exceed 64 characters') + } + + // 不能以连字符开头或结尾 + if (trimmedUsername.startsWith('-') || trimmedUsername.endsWith('-')) { + throw new Error('Username cannot start or end with a hyphen') + } + + return trimmedUsername } // 🔐 主要的登录验证方法 @@ -728,21 +488,10 @@ class LdapService { // 5. 提取用户信息 const userInfo = this.extractUserInfo(ldapEntry, sanitizedUsername) - // 6. Windows AD 特定检查:验证账户是否被禁用 - if (this.isActiveDirectory && userInfo.accountDisabled) { - logger.security( - `🔒 Disabled Windows AD account login attempt: ${sanitizedUsername} from LDAP authentication` - ) - return { - success: false, - message: 'Your account has been disabled. Please contact administrator.' - } - } - - // 7. 创建或更新本地用户 + // 6. 创建或更新本地用户 const user = await userService.createOrUpdateUser(userInfo) - // 8. 检查用户是否被禁用 + // 7. 检查用户是否被禁用 if (!user.isActive) { logger.security( `🔒 Disabled user LDAP login attempt: ${sanitizedUsername} from LDAP authentication` @@ -753,15 +502,13 @@ class LdapService { } } - // 9. 记录登录 + // 8. 记录登录 await userService.recordUserLogin(user.id) - // 10. 创建用户会话 + // 9. 创建用户会话 const sessionToken = await userService.createUserSession(user.id) - logger.info( - `✅ LDAP authentication successful for user: ${sanitizedUsername} (${this.serverType})` - ) + logger.info(`✅ LDAP authentication successful for user: ${sanitizedUsername}`) return { success: true, @@ -851,8 +598,6 @@ class LdapService { getConfigInfo() { const configInfo = { enabled: this.config.enabled, - serverType: this.serverType, - isActiveDirectory: this.isActiveDirectory, server: { url: this.config.server.url, searchBase: this.config.server.searchBase, @@ -874,16 +619,6 @@ class LdapService { } } - // Windows AD 特定配置信息 - if (this.isActiveDirectory) { - configInfo.activeDirectoryFeatures = { - supportsUPN: true, - supportsDomainUsername: true, - supportsGlobalCatalog: true, - checksAccountDisabled: true - } - } - return configInfo } } From bec9cf565bf80d2918c03f2059fd0404f56260a5 Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Wed, 3 Sep 2025 15:15:13 +0800 Subject: [PATCH 5/5] feat: add Windows Active Directory LDAP authentication support --- src/services/ldapService.js | 133 +++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/src/services/ldapService.js b/src/services/ldapService.js index 75b4e704..86fdb88d 100644 --- a/src/services/ldapService.js +++ b/src/services/ldapService.js @@ -97,6 +97,38 @@ class LdapService { return null } + // 🌐 从DN中提取域名,用于Windows AD UPN格式认证 + extractDomainFromDN(dnString) { + try { + if (!dnString || typeof dnString !== 'string') { + return null + } + + // 提取所有DC组件:DC=test,DC=demo,DC=com + const dcMatches = dnString.match(/DC=([^,]+)/gi) + if (!dcMatches || dcMatches.length === 0) { + return null + } + + // 提取DC值并连接成域名 + const domainParts = dcMatches.map((match) => { + const value = match.replace(/DC=/i, '').trim() + return value + }) + + if (domainParts.length > 0) { + const domain = domainParts.join('.') + logger.debug(`🌐 从DN提取域名: ${domain}`) + return domain + } + + return null + } catch (error) { + logger.debug('⚠️ 域名提取失败:', error.message) + return null + } + } + // 🔗 创建LDAP客户端连接 createClient() { try { @@ -336,6 +368,79 @@ class LdapService { }) } + // 🔐 Windows AD兼容认证 - 在DN认证失败时尝试多种格式 + async tryWindowsADAuthentication(username, password) { + if (!username || !password) { + return false + } + + // 从searchBase提取域名 + const domain = this.extractDomainFromDN(this.config.server.searchBase) + + const adFormats = [] + + if (domain) { + // UPN格式(Windows AD标准) + adFormats.push(`${username}@${domain}`) + + // 如果域名有多个部分,也尝试简化版本 + const domainParts = domain.split('.') + if (domainParts.length > 1) { + adFormats.push(`${username}@${domainParts.slice(-2).join('.')}`) // 只取后两部分 + } + + // 域\用户名格式 + const firstDomainPart = domainParts[0] + if (firstDomainPart) { + adFormats.push(`${firstDomainPart}\\${username}`) + adFormats.push(`${firstDomainPart.toUpperCase()}\\${username}`) + } + } + + // 纯用户名(最后尝试) + adFormats.push(username) + + logger.info(`🔄 尝试 ${adFormats.length} 种Windows AD认证格式...`) + + for (const format of adFormats) { + try { + logger.info(`🔍 尝试格式: ${format}`) + const result = await this.tryDirectBind(format, password) + if (result) { + logger.info(`✅ Windows AD认证成功: ${format}`) + return true + } + logger.debug(`❌ 认证失败: ${format}`) + } catch (error) { + logger.debug(`认证异常 ${format}:`, error.message) + } + } + + logger.info(`🚫 所有Windows AD格式认证都失败了`) + return false + } + + // 🔐 直接尝试绑定认证的辅助方法 + async tryDirectBind(identifier, password) { + return new Promise((resolve, reject) => { + const authClient = this.createClient() + + authClient.bind(identifier, password, (err) => { + authClient.unbind() + + if (err) { + if (err.name === 'InvalidCredentialsError') { + resolve(false) + } else { + reject(err) + } + } else { + resolve(true) + } + }) + }) + } + // 📝 提取用户信息 extractUserInfo(ldapEntry, username) { try { @@ -478,10 +583,32 @@ class LdapService { return { success: false, message: 'Authentication service error' } } - // 4. 验证用户密码 - const isPasswordValid = await this.authenticateUser(userDN, password) + // 4. 验证用户密码 - 支持传统LDAP和Windows AD + let isPasswordValid = false + + // 首先尝试传统的DN认证(保持原有LDAP逻辑) + try { + isPasswordValid = await this.authenticateUser(userDN, password) + if (isPasswordValid) { + logger.info(`✅ DN authentication successful for user: ${sanitizedUsername}`) + } + } catch (error) { + logger.debug( + `DN authentication failed for user: ${sanitizedUsername}, error: ${error.message}` + ) + } + + // 如果DN认证失败,尝试Windows AD多格式认证 if (!isPasswordValid) { - logger.info(`🚫 Invalid password for user: ${sanitizedUsername}`) + logger.debug(`🔄 Trying Windows AD authentication formats for user: ${sanitizedUsername}`) + isPasswordValid = await this.tryWindowsADAuthentication(sanitizedUsername, password) + if (isPasswordValid) { + logger.info(`✅ Windows AD authentication successful for user: ${sanitizedUsername}`) + } + } + + if (!isPasswordValid) { + logger.info(`🚫 All authentication methods failed for user: ${sanitizedUsername}`) return { success: false, message: 'Invalid username or password' } }