feat: 完整实现AD域控用户认证系统

主要功能:
- 新增LDAP服务连接AD域控服务器
- 实现多格式AD用户认证(sAMAccountName, UPN, 域\用户名, DN)
- 支持中文显示名和拼音用户名搜索
- 添加用户账户状态检查(禁用账户检测)
- 实现JWT token认证和用户会话管理

新增文件:
- src/services/ldapService.js - LDAP核心服务
- src/routes/ldapRoutes.js - AD认证API路由
- src/services/userMappingService.js - 用户映射服务
- web/admin-spa/src/views/UserDashboardView.vue - 用户控制台
- web/admin-spa/src/components/user/ - 用户组件目录

修改功能:
- ApiStatsView.vue 增加用户登录按钮和模态框
- 路由系统增加用户专用页面
- 安装ldapjs和jsonwebtoken依赖

技术特性:
- 多种认证格式自动尝试
- LDAP referral错误处理
- 详细认证日志和错误码记录
- 前后端完整用户认证流程

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
iRubbish
2025-08-25 18:03:55 +08:00
parent 5c5548e839
commit 7624c383e8
12 changed files with 3037 additions and 4 deletions

562
src/routes/ldapRoutes.js Normal file
View File

@@ -0,0 +1,562 @@
const express = require('express')
const ldapService = require('../services/ldapService')
const logger = require('../utils/logger')
const router = express.Router()
/**
* 测试LDAP/AD连接
*/
router.get('/test-connection', async (req, res) => {
try {
logger.info('LDAP connection test requested')
const result = await ldapService.testConnection()
if (result.success) {
res.json({
success: true,
message: 'LDAP/AD connection successful',
data: result
})
} else {
res.status(500).json({
success: false,
message: 'LDAP/AD connection failed',
error: result.error,
config: result.config
})
}
} catch (error) {
logger.error('LDAP connection test error:', error)
res.status(500).json({
success: false,
message: 'LDAP connection test failed',
error: error.message
})
}
})
/**
* 获取LDAP配置信息
*/
router.get('/config', (req, res) => {
try {
const config = ldapService.getConfig()
res.json({
success: true,
config
})
} catch (error) {
logger.error('Get LDAP config error:', error)
res.status(500).json({
success: false,
message: 'Failed to get LDAP config',
error: error.message
})
}
})
/**
* 搜索用户
*/
router.post('/search-user', async (req, res) => {
try {
const { username } = req.body
if (!username) {
return res.status(400).json({
success: false,
message: 'Username is required'
})
}
logger.info(`Searching for user: ${username}`)
await ldapService.createConnection()
await ldapService.bind()
const users = await ldapService.searchUser(username)
res.json({
success: true,
message: `Found ${users.length} users`,
users
})
} catch (error) {
logger.error('User search error:', error)
res.status(500).json({
success: false,
message: 'User search failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 列出所有用户模拟Python代码的describe_ou功能
*/
router.get('/list-users', async (req, res) => {
try {
const { limit = 20, type = 'human' } = req.query
const limitNum = parseInt(limit)
logger.info(`Listing users with limit: ${limitNum}, type: ${type}`)
await ldapService.createConnection()
await ldapService.bind()
const users = await ldapService.listAllUsers(limitNum, type)
res.json({
success: true,
message: `Found ${users.length} users`,
users,
total: users.length,
limit: limitNum,
type
})
} catch (error) {
logger.error('List users error:', error)
res.status(500).json({
success: false,
message: 'List users failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 测试用户认证
*/
router.post('/test-auth', async (req, res) => {
try {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({
success: false,
message: 'Username and password are required'
})
}
logger.info(`Testing authentication for user: ${username}`)
const result = await ldapService.authenticateUser(username, password)
res.json({
success: true,
message: 'Authentication successful',
user: result.user
})
} catch (error) {
logger.error('User authentication test error:', error)
res.status(401).json({
success: false,
message: 'Authentication failed',
error: error.message
})
}
})
/**
* 列出所有OU
*/
router.get('/list-ous', async (req, res) => {
try {
logger.info('Listing all OUs in domain')
await ldapService.createConnection()
await ldapService.bind()
const ous = await ldapService.listOUs()
res.json({
success: true,
message: `Found ${ous.length} OUs`,
ous
})
} catch (error) {
logger.error('List OUs error:', error)
res.status(500).json({
success: false,
message: 'List OUs failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 验证OU是否存在
*/
router.get('/verify-ou', async (req, res) => {
try {
const { ou = '微店' } = req.query
const testDN = `OU=${ou},DC=corp,DC=weidian-inc,DC=com`
logger.info(`Verifying OU exists: ${testDN}`)
await ldapService.createConnection()
await ldapService.bind()
const result = await ldapService.verifyOU(testDN)
res.json({
success: true,
message: 'OU verification completed',
testDN,
result
})
} catch (error) {
logger.error('OU verification error:', error)
res.status(500).json({
success: false,
message: 'OU verification failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* LDAP服务状态检查
*/
router.get('/status', async (req, res) => {
try {
const config = ldapService.getConfig()
// 简单的连接测试
const connectionTest = await ldapService.testConnection()
res.json({
success: true,
status: connectionTest.success ? 'connected' : 'disconnected',
config,
lastTest: new Date().toISOString(),
testResult: connectionTest
})
} catch (error) {
logger.error('LDAP status check error:', error)
res.status(500).json({
success: false,
status: 'error',
message: 'Status check failed',
error: error.message
})
}
})
/**
* AD用户登录认证
*/
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
})
}
logger.info(`AD用户登录尝试: ${username}`)
// 使用AD认证用户
const authResult = await ldapService.authenticateUser(username, password)
// 生成用户会话token
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const userInfo = {
type: 'ad_user',
username: authResult.user.username || authResult.user.cn,
displayName: authResult.user.displayName,
email: authResult.user.email,
groups: authResult.user.groups,
loginTime: new Date().toISOString()
}
const token = jwt.sign(userInfo, config.security.jwtSecret, {
expiresIn: '8h' // 8小时过期
})
logger.info(`AD用户登录成功: ${username}`)
res.json({
success: true,
message: '登录成功',
token,
user: userInfo
})
} catch (error) {
logger.error('AD用户登录失败:', error)
res.status(401).json({
success: false,
message: '用户名或密码错误',
error: error.message
})
}
})
/**
* AD用户token验证
*/
router.get('/verify-token', (req, res) => {
try {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '未提供有效的认证token'
})
}
const token = authHeader.substring(7)
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const decoded = jwt.verify(token, config.security.jwtSecret)
if (decoded.type !== 'ad_user') {
return res.status(403).json({
success: false,
message: '无效的用户类型'
})
}
res.json({
success: true,
user: decoded
})
} catch (error) {
logger.error('Token验证失败:', error)
res.status(401).json({
success: false,
message: 'Token无效或已过期'
})
}
})
/**
* AD用户认证中间件
*/
const authenticateUser = (req, res, next) => {
try {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '未提供有效的认证token'
})
}
const token = authHeader.substring(7)
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const decoded = jwt.verify(token, config.security.jwtSecret)
if (decoded.type !== 'ad_user') {
return res.status(403).json({
success: false,
message: '无效的用户类型'
})
}
req.user = decoded
next()
} catch (error) {
logger.error('用户认证失败:', error)
res.status(401).json({
success: false,
message: 'Token无效或已过期'
})
}
}
/**
* 获取用户的API Keys
*/
router.get('/user/api-keys', authenticateUser, async (req, res) => {
try {
const redis = require('../models/redis')
const { username } = req.user
logger.info(`获取用户API Keys: ${username}`)
// 获取所有API Keys
const allKeysPattern = 'api_key:*'
const keys = await redis.getClient().keys(allKeysPattern)
const userKeys = []
// 筛选属于该用户的API Keys
for (const key of keys) {
const apiKeyData = await redis.getClient().hgetall(key)
if (apiKeyData && apiKeyData.owner === username) {
userKeys.push({
id: apiKeyData.id,
name: apiKeyData.name || '未命名',
key: apiKeyData.key,
limit: parseInt(apiKeyData.limit) || 1000000,
used: parseInt(apiKeyData.used) || 0,
createdAt: apiKeyData.createdAt,
status: apiKeyData.status || 'active'
})
}
}
res.json({
success: true,
apiKeys: userKeys
})
} catch (error) {
logger.error('获取用户API Keys失败:', error)
res.status(500).json({
success: false,
message: '获取API Keys失败'
})
}
})
/**
* 创建用户API Key
*/
router.post('/user/api-keys', authenticateUser, async (req, res) => {
try {
const { username } = req.user
const { name, limit } = req.body
// 检查用户是否已有API Key
const redis = require('../models/redis')
const allKeysPattern = 'api_key:*'
const keys = await redis.getClient().keys(allKeysPattern)
let userKeyCount = 0
for (const key of keys) {
const apiKeyData = await redis.getClient().hgetall(key)
if (apiKeyData && apiKeyData.owner === username) {
userKeyCount++
}
}
if (userKeyCount >= 1) {
return res.status(400).json({
success: false,
message: '每个用户只能创建一个API Key'
})
}
// 生成API Key
const crypto = require('crypto')
const uuid = require('uuid')
const keyId = uuid.v4()
const apiKey = `cr_${crypto.randomBytes(32).toString('hex')}`
const keyData = {
id: keyId,
key: apiKey,
name: name || 'AD用户密钥',
limit: limit || 100000,
used: 0,
owner: username,
ownerType: 'ad_user',
createdAt: new Date().toISOString(),
status: 'active'
}
// 存储到Redis
await redis.getClient().hset(`api_key:${keyId}`, keyData)
// 创建哈希映射以快速查找
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex')
await redis.getClient().set(`api_key_hash:${keyHash}`, keyId)
logger.info(`用户${username}创建API Key成功: ${keyId}`)
res.json({
success: true,
message: 'API Key创建成功',
apiKey: {
id: keyId,
key: apiKey,
name: keyData.name,
limit: keyData.limit,
used: 0,
createdAt: keyData.createdAt,
status: keyData.status
}
})
} catch (error) {
logger.error('创建用户API Key失败:', error)
res.status(500).json({
success: false,
message: '创建API Key失败'
})
}
})
/**
* 获取用户API Key使用统计
*/
router.get('/user/usage-stats', authenticateUser, async (req, res) => {
try {
const { username } = req.user
const redis = require('../models/redis')
// 获取用户的API Keys
const allKeysPattern = 'api_key:*'
const keys = await redis.getClient().keys(allKeysPattern)
let totalUsage = 0
let totalLimit = 0
const userKeys = []
for (const key of keys) {
const apiKeyData = await redis.getClient().hgetall(key)
if (apiKeyData && apiKeyData.owner === username) {
const used = parseInt(apiKeyData.used) || 0
const limit = parseInt(apiKeyData.limit) || 0
totalUsage += used
totalLimit += limit
userKeys.push({
id: apiKeyData.id,
name: apiKeyData.name,
used,
limit,
percentage: limit > 0 ? Math.round((used / limit) * 100) : 0
})
}
}
res.json({
success: true,
stats: {
totalUsage,
totalLimit,
percentage: totalLimit > 0 ? Math.round((totalUsage / totalLimit) * 100) : 0,
keyCount: userKeys.length,
keys: userKeys
}
})
} catch (error) {
logger.error('获取用户使用统计失败:', error)
res.status(500).json({
success: false,
message: '获取使用统计失败'
})
}
})
module.exports = router