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] 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 } }