mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
add support of Windows AD Server
This commit is contained in:
20
.env.example
20
.env.example
@@ -64,11 +64,16 @@ TRUST_PROXY=true
|
|||||||
|
|
||||||
# 🔐 LDAP 认证配置
|
# 🔐 LDAP 认证配置
|
||||||
LDAP_ENABLED=false
|
LDAP_ENABLED=false
|
||||||
|
# 服务器类型:openldap 或 activedirectory
|
||||||
|
LDAP_SERVER_TYPE=openldap
|
||||||
|
# LDAP 服务器配置
|
||||||
LDAP_URL=ldaps://ldap-1.test1.bj.yxops.net:636
|
LDAP_URL=ldaps://ldap-1.test1.bj.yxops.net:636
|
||||||
LDAP_BIND_DN=cn=admin,dc=example,dc=com
|
LDAP_BIND_DN=cn=admin,dc=example,dc=com
|
||||||
LDAP_BIND_PASSWORD=admin_password
|
LDAP_BIND_PASSWORD=admin_password
|
||||||
LDAP_SEARCH_BASE=dc=example,dc=com
|
LDAP_SEARCH_BASE=dc=example,dc=com
|
||||||
|
# 搜索过滤器 (OpenLDAP 使用 uid,AD 会自动使用 sAMAccountName/userPrincipalName)
|
||||||
LDAP_SEARCH_FILTER=(uid={{username}})
|
LDAP_SEARCH_FILTER=(uid={{username}})
|
||||||
|
# 搜索属性 (根据服务器类型自动设置,也可手动指定)
|
||||||
LDAP_SEARCH_ATTRIBUTES=dn,uid,cn,mail,givenName,sn
|
LDAP_SEARCH_ATTRIBUTES=dn,uid,cn,mail,givenName,sn
|
||||||
LDAP_TIMEOUT=5000
|
LDAP_TIMEOUT=5000
|
||||||
LDAP_CONNECT_TIMEOUT=10000
|
LDAP_CONNECT_TIMEOUT=10000
|
||||||
@@ -85,13 +90,26 @@ LDAP_TLS_REJECT_UNAUTHORIZED=true
|
|||||||
# 服务器名称 (可选,用于 SNI)
|
# 服务器名称 (可选,用于 SNI)
|
||||||
# LDAP_TLS_SERVERNAME=ldap.example.com
|
# LDAP_TLS_SERVERNAME=ldap.example.com
|
||||||
|
|
||||||
# 🗺️ LDAP 用户属性映射
|
# 🗺️ LDAP 用户属性映射 (根据服务器类型自动设置默认值)
|
||||||
LDAP_USER_ATTR_USERNAME=uid
|
LDAP_USER_ATTR_USERNAME=uid
|
||||||
LDAP_USER_ATTR_DISPLAY_NAME=cn
|
LDAP_USER_ATTR_DISPLAY_NAME=cn
|
||||||
LDAP_USER_ATTR_EMAIL=mail
|
LDAP_USER_ATTR_EMAIL=mail
|
||||||
LDAP_USER_ATTR_FIRST_NAME=givenName
|
LDAP_USER_ATTR_FIRST_NAME=givenName
|
||||||
LDAP_USER_ATTR_LAST_NAME=sn
|
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
|
USER_MANAGEMENT_ENABLED=false
|
||||||
DEFAULT_USER_ROLE=user
|
DEFAULT_USER_ROLE=user
|
||||||
|
|||||||
@@ -130,14 +130,20 @@ const config = {
|
|||||||
// 🔐 LDAP 认证配置
|
// 🔐 LDAP 认证配置
|
||||||
ldap: {
|
ldap: {
|
||||||
enabled: process.env.LDAP_ENABLED === 'true',
|
enabled: process.env.LDAP_ENABLED === 'true',
|
||||||
|
// 服务器类型:'openldap' 或 'activedirectory'
|
||||||
|
serverType: process.env.LDAP_SERVER_TYPE || 'openldap',
|
||||||
server: {
|
server: {
|
||||||
url: process.env.LDAP_URL || 'ldap://localhost:389',
|
url: process.env.LDAP_URL || 'ldap://localhost:389',
|
||||||
bindDN: process.env.LDAP_BIND_DN || 'cn=admin,dc=example,dc=com',
|
bindDN: process.env.LDAP_BIND_DN || 'cn=admin,dc=example,dc=com',
|
||||||
bindCredentials: process.env.LDAP_BIND_PASSWORD || 'admin',
|
bindCredentials: process.env.LDAP_BIND_PASSWORD || 'admin',
|
||||||
searchBase: process.env.LDAP_SEARCH_BASE || 'dc=example,dc=com',
|
searchBase: process.env.LDAP_SEARCH_BASE || 'dc=example,dc=com',
|
||||||
|
// 搜索过滤器 - OpenLDAP 默认使用 uid,Windows AD 会自动使用 sAMAccountName/userPrincipalName
|
||||||
searchFilter: process.env.LDAP_SEARCH_FILTER || '(uid={{username}})',
|
searchFilter: process.env.LDAP_SEARCH_FILTER || '(uid={{username}})',
|
||||||
|
// 搜索属性 - 根据服务器类型自动设置默认值
|
||||||
searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES
|
searchAttributes: process.env.LDAP_SEARCH_ATTRIBUTES
|
||||||
? process.env.LDAP_SEARCH_ATTRIBUTES.split(',')
|
? 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', 'uid', 'cn', 'mail', 'givenName', 'sn'],
|
||||||
timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000,
|
timeout: parseInt(process.env.LDAP_TIMEOUT) || 5000,
|
||||||
connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000,
|
connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000,
|
||||||
@@ -161,9 +167,10 @@ const config = {
|
|||||||
servername: process.env.LDAP_TLS_SERVERNAME || undefined
|
servername: process.env.LDAP_TLS_SERVERNAME || undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 用户属性映射 - 根据服务器类型自动设置默认值
|
||||||
userMapping: {
|
userMapping: {
|
||||||
username: process.env.LDAP_USER_ATTR_USERNAME || 'uid',
|
username: process.env.LDAP_USER_ATTR_USERNAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'sAMAccountName' : 'uid'),
|
||||||
displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || 'cn',
|
displayName: process.env.LDAP_USER_ATTR_DISPLAY_NAME || (process.env.LDAP_SERVER_TYPE === 'activedirectory' ? 'displayName' : 'cn'),
|
||||||
email: process.env.LDAP_USER_ATTR_EMAIL || 'mail',
|
email: process.env.LDAP_USER_ATTR_EMAIL || 'mail',
|
||||||
firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName',
|
firstName: process.env.LDAP_USER_ATTR_FIRST_NAME || 'givenName',
|
||||||
lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn'
|
lastName: process.env.LDAP_USER_ATTR_LAST_NAME || 'sn'
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ class LdapService {
|
|||||||
this.config = config.ldap || {}
|
this.config = config.ldap || {}
|
||||||
this.client = null
|
this.client = null
|
||||||
|
|
||||||
|
// 设置服务器类型,默认为 OpenLDAP
|
||||||
|
this.serverType = this.config.serverType || 'openldap'
|
||||||
|
this.isActiveDirectory = this.serverType === 'activedirectory'
|
||||||
|
|
||||||
// 验证配置 - 只有在 LDAP 配置存在且启用时才验证
|
// 验证配置 - 只有在 LDAP 配置存在且启用时才验证
|
||||||
if (this.config && this.config.enabled) {
|
if (this.config && this.config.enabled) {
|
||||||
this.validateConfiguration()
|
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
|
// 🔍 提取LDAP条目的DN
|
||||||
extractDN(ldapEntry) {
|
extractDN(ldapEntry) {
|
||||||
if (!ldapEntry) {
|
if (!ldapEntry) {
|
||||||
@@ -219,9 +310,12 @@ class LdapService {
|
|||||||
// 🔍 搜索用户
|
// 🔍 搜索用户
|
||||||
async searchUser(client, username) {
|
async searchUser(client, username) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// 解析用户名(对Windows AD进行特殊处理)
|
||||||
|
const usernameInfo = this.parseActiveDirectoryUsername(username)
|
||||||
|
|
||||||
// 防止LDAP注入:转义特殊字符
|
// 防止LDAP注入:转义特殊字符
|
||||||
// 根据RFC 4515,需要转义的特殊字符:* ( ) \ NUL
|
// 根据RFC 4515,需要转义的特殊字符:* ( ) \ NUL
|
||||||
const escapedUsername = username
|
const escapedUsername = usernameInfo.username
|
||||||
.replace(/\\/g, '\\5c') // 反斜杠必须先转义
|
.replace(/\\/g, '\\5c') // 反斜杠必须先转义
|
||||||
.replace(/\*/g, '\\2a') // 星号
|
.replace(/\*/g, '\\2a') // 星号
|
||||||
.replace(/\(/g, '\\28') // 左括号
|
.replace(/\(/g, '\\28') // 左括号
|
||||||
@@ -229,14 +323,41 @@ class LdapService {
|
|||||||
.replace(/\0/g, '\\00') // NUL字符
|
.replace(/\0/g, '\\00') // NUL字符
|
||||||
.replace(/\//g, '\\2f') // 斜杠
|
.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 = {
|
const searchOptions = {
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
filter: searchFilter,
|
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 = []
|
const entries = []
|
||||||
|
|
||||||
@@ -254,7 +375,8 @@ class LdapService {
|
|||||||
type: typeof entry.dn,
|
type: typeof entry.dn,
|
||||||
entryType: typeof entry,
|
entryType: typeof entry,
|
||||||
hasAttributes: !!entry.attributes,
|
hasAttributes: !!entry.attributes,
|
||||||
attributeCount: entry.attributes ? entry.attributes.length : 0
|
attributeCount: entry.attributes ? entry.attributes.length : 0,
|
||||||
|
serverType: this.serverType
|
||||||
})
|
})
|
||||||
entries.push(entry)
|
entries.push(entry)
|
||||||
})
|
})
|
||||||
@@ -270,7 +392,7 @@ class LdapService {
|
|||||||
|
|
||||||
res.on('end', (result) => {
|
res.on('end', (result) => {
|
||||||
logger.debug(
|
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) {
|
if (entries.length === 0) {
|
||||||
@@ -282,14 +404,17 @@ class LdapService {
|
|||||||
entryType: typeof entries[0],
|
entryType: typeof entries[0],
|
||||||
entryConstructor: entries[0].constructor?.name,
|
entryConstructor: entries[0].constructor?.name,
|
||||||
entryKeys: Object.keys(entries[0]),
|
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) {
|
if (entries.length === 1) {
|
||||||
resolve(entries[0])
|
resolve(entries[0])
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`⚠️ Multiple LDAP entries found for username: ${username}`)
|
logger.warn(
|
||||||
|
`⚠️ Multiple LDAP entries found for username: ${username} (${this.serverType})`
|
||||||
|
)
|
||||||
resolve(entries[0]) // 使用第一个结果
|
resolve(entries[0]) // 使用第一个结果
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,13 +475,70 @@ class LdapService {
|
|||||||
attrMap[name] = values.length === 1 ? values[0] : values
|
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
|
// 显示名称:优先使用displayName,其次cn
|
||||||
userInfo.email = attrMap[mapping.email] || ''
|
userInfo.displayName =
|
||||||
userInfo.firstName = attrMap[mapping.firstName] || ''
|
attrMap[mapping.displayName || 'displayName'] ||
|
||||||
userInfo.lastName = attrMap[mapping.lastName] || ''
|
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
|
// 如果没有displayName,尝试组合firstName和lastName
|
||||||
if (!userInfo.displayName || userInfo.displayName === username) {
|
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
|
return userInfo
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Error extracting user info:', error)
|
logger.error('❌ Error extracting user info:', error)
|
||||||
@@ -386,6 +562,69 @@ class LdapService {
|
|||||||
|
|
||||||
const trimmedUsername = username.trim()
|
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_-]+$/
|
const usernameRegex = /^[a-zA-Z0-9_-]+$/
|
||||||
if (!usernameRegex.test(trimmedUsername)) {
|
if (!usernameRegex.test(trimmedUsername)) {
|
||||||
@@ -404,6 +643,7 @@ class LdapService {
|
|||||||
|
|
||||||
return trimmedUsername
|
return trimmedUsername
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔐 主要的登录验证方法
|
// 🔐 主要的登录验证方法
|
||||||
async authenticateUserCredentials(username, password) {
|
async authenticateUserCredentials(username, password) {
|
||||||
@@ -488,10 +728,21 @@ class LdapService {
|
|||||||
// 5. 提取用户信息
|
// 5. 提取用户信息
|
||||||
const userInfo = this.extractUserInfo(ldapEntry, sanitizedUsername)
|
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)
|
const user = await userService.createOrUpdateUser(userInfo)
|
||||||
|
|
||||||
// 7. 检查用户是否被禁用
|
// 8. 检查用户是否被禁用
|
||||||
if (!user.isActive) {
|
if (!user.isActive) {
|
||||||
logger.security(
|
logger.security(
|
||||||
`🔒 Disabled user LDAP login attempt: ${sanitizedUsername} from LDAP authentication`
|
`🔒 Disabled user LDAP login attempt: ${sanitizedUsername} from LDAP authentication`
|
||||||
@@ -502,13 +753,15 @@ class LdapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 记录登录
|
// 9. 记录登录
|
||||||
await userService.recordUserLogin(user.id)
|
await userService.recordUserLogin(user.id)
|
||||||
|
|
||||||
// 9. 创建用户会话
|
// 10. 创建用户会话
|
||||||
const sessionToken = await userService.createUserSession(user.id)
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -598,6 +851,8 @@ class LdapService {
|
|||||||
getConfigInfo() {
|
getConfigInfo() {
|
||||||
const configInfo = {
|
const configInfo = {
|
||||||
enabled: this.config.enabled,
|
enabled: this.config.enabled,
|
||||||
|
serverType: this.serverType,
|
||||||
|
isActiveDirectory: this.isActiveDirectory,
|
||||||
server: {
|
server: {
|
||||||
url: this.config.server.url,
|
url: this.config.server.url,
|
||||||
searchBase: this.config.server.searchBase,
|
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
|
return configInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user