Revert "add support of Windows AD Server"

This reverts commit a1005e91c8.
This commit is contained in:
Feng Yue
2025-09-03 15:03:14 +08:00
parent 0039569471
commit f69333f312
3 changed files with 44 additions and 334 deletions

View File

@@ -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 使用 uidAD 会自动使用 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

View File

@@ -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 默认使用 uidWindows 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'

View File

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