diff --git a/README.md b/README.md index 07462be..ca75bf5 100644 --- a/README.md +++ b/README.md @@ -434,3 +434,122 @@ flexibility. # Acknowledgements * This product includes software developed by the Thomson Reuters Global Resources. ([go-ntlm](https://github.com/m7913d/go-ntlm) - BSD-4 License) + +# RDPGW 认证 API 服务器 + +这是一个用于 RDPGW(远程桌面网关)的认证 API 服务器,提供用户验证和密码获取功能。 + +## 功能特性 + +- 支持用户名和密码验证(verify 模式) +- 支持密码检索功能(getpassword 模式),用于 NTLM 认证 +- 支持 GET 和 POST 请求方式 +- 可通过配置文件自定义设置 + +## 安装 + +确保已安装 Node.js v10 或更高版本,然后执行: + +```bash +npm install express +``` + +## 配置 + +配置文件为 `config.json`,包含以下选项: + +```json +{ + "port": 3000, + "apiPath": "/api/checkperm", + "users": { + "testuser": "testpassword", + "admin": "adminpass" + }, + "logger": { + "level": "info", + "logToFile": false, + "logFile": "server.log" + } +} +``` + +### 配置选项说明 + +- `port`: 服务器监听端口 +- `apiPath`: API 路径 +- `users`: 用户名和密码字典 +- `logger`: 日志配置 + +## 使用方法 + +### 启动服务器 + +```bash +node index.js +``` + +### API 请求示例 + +#### 验证模式 (verify) + +**GET 请求**: +``` +http://localhost:3000/api/checkperm?username=testuser&password=testpassword&mode=verify +``` + +**POST 请求**: +```bash +curl -X POST http://localhost:3000/api/checkperm \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser", "password":"testpassword", "mode":"verify"}' +``` + +**响应**: +```json +{ + "status": "success", + "user": "testuser" +} +``` + +#### 密码获取模式 (getpassword) + +**GET 请求**: +``` +http://localhost:3000/api/checkperm?username=testuser&mode=getpassword +``` + +**POST 请求**: +```bash +curl -X POST http://localhost:3000/api/checkperm \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser", "mode":"getpassword"}' +``` + +**响应**: +```json +{ + "status": "success", + "password": "testpassword" +} +``` + +## RDPGW 集成 + +在 RDPGW 配置中添加以下内容: + +```yaml +ntlm_api: + enable: true + server: http://localhost:3000 + path: /api/checkperm + mode: getpassword +``` + +## 安全建议 + +- 在生产环境中,请使用 HTTPS 而非 HTTP +- 限制 API 服务器的访问权限 +- 定期更换密码并审查访问日志 +- 考虑实现 IP 访问限制和请求频率限制 diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index ede837a..481eee5 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -133,8 +133,21 @@ func main() { if err != nil { log.Fatal(err) } + server := grpc.NewServer() - db := database.NewConfig(conf.Users) + + // 根据配置选择使用API认证或本地配置认证 + var db database.Database + log.Printf("使用API认证,API URL: %s", conf.ApiAuth.ApiUrl) + log.Printf("使用API认证,API URL: %s", conf.ApiAuth.Enabled) + if conf.ApiAuth.Enabled && conf.ApiAuth.ApiUrl != "" { + log.Printf("使用API认证,API URL: %s", conf.ApiAuth.ApiUrl) + db = database.NewApiDb(conf.ApiAuth.ApiUrl) + } else { + log.Printf("使用本地配置文件认证") + db = database.NewConfig(conf.Users) + } + service := NewAuthService(opts.ServiceName, db) auth.RegisterAuthenticateServer(server, service) server.Serve(listener) diff --git a/cmd/auth/config/configuration.go b/cmd/auth/config/configuration.go index 580b7f2..b13683f 100644 --- a/cmd/auth/config/configuration.go +++ b/cmd/auth/config/configuration.go @@ -11,6 +11,12 @@ import ( type Configuration struct { Users []UserConfig `koanf:"users"` + ApiAuth ApiAuthConfig `koanf:"apiauth"` +} + +type ApiAuthConfig struct { + Enabled bool `koanf:"enabled"` + ApiUrl string `koanf:"apiurl"` } type UserConfig struct { @@ -36,6 +42,7 @@ func Load(configFile string) Configuration { koanfTag := koanf.UnmarshalConf{Tag: "koanf"} k.UnmarshalWithConf("Users", &Conf.Users, koanfTag) + k.UnmarshalWithConf("ApiAuth", &Conf.ApiAuth, koanfTag) return Conf diff --git a/cmd/auth/database/apidb.go b/cmd/auth/database/apidb.go new file mode 100644 index 0000000..bf5ef3e --- /dev/null +++ b/cmd/auth/database/apidb.go @@ -0,0 +1,169 @@ +package database + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" +) + +// ApiDb 结构实现Database接口,通过API验证用户凭据 +type ApiDb struct { + ApiUrl string // API URL模板,使用{username}和{password}作为占位符 +} + +// NewApiDb 创建一个新的ApiDb实例 +func NewApiDb(apiUrl string) *ApiDb { + return &ApiDb{ + ApiUrl: apiUrl, + } +} + +// GetPassword 从API获取用户密码 +// 这个方法会调用API来获取用户的实际密码,用于NTLM认证 +func (a *ApiDb) GetPassword(username string) string { + log.Printf("Getpassword: %s", username) + + // 如果用户名为空,直接返回失败 + if username == "" { + log.Printf("API password retrieval failed: empty username") + return "" + } + + // 构建API URL,替换占位符 + apiUrl := a.ApiUrl + + // 构建完整的URL,包括查询参数 - 使用getpassword模式 + fullUrl := fmt.Sprintf("%s?username=%s&mode=getpassword", + apiUrl, url.QueryEscape(username)) + + log.Printf("Sending API password request to: %s", fullUrl) + + // 创建自定义HTTP客户端,跳过SSL证书验证 + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + + // 发送请求到API,使用不验证SSL证书的客户端 + resp, err := client.Get(fullUrl) + if err != nil { + log.Printf("API password retrieval error: %v", err) + return "" + } + defer resp.Body.Close() + + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + log.Printf("API password retrieval failed with status: %d", resp.StatusCode) + return "" + } + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Failed to read API response: %v", err) + return "" + } + log.Printf("API password response received") + + // 解析响应 + var result struct { + Status string `json:"status"` + Password string `json:"password"` + } + + err = json.NewDecoder(bytes.NewReader(body)).Decode(&result) + if err != nil { + log.Printf("Failed to parse API response: %v", err) + return "" + } + + // 检查响应内容 + if result.Status != "success" || result.Password == "" { + log.Printf("API did not return a valid password for user: %s", username) + return "" + } + + log.Printf("API password retrieval successful for user: %s", username) + return result.Password +} + +// VerifyCredentials 验证用户凭据 +func (a *ApiDb) VerifyCredentials(username, password string) bool { + // 如果用户名为空,直接返回失败 + if username == "" { + log.Printf("API verification failed: empty username") + return false + } + + // 构建API URL,替换占位符 + apiUrl := a.ApiUrl + + // 构建完整的URL,包括查询参数 + var fullUrl string + if password == "" { + // NTLM场景下直接将用户名传递给API + // 这种情况下,后端API应当能够独立验证用户 + fullUrl = fmt.Sprintf("%s?username=%s&mode=verify", + apiUrl, url.QueryEscape(username)) + log.Printf("Verifying NTLM user via API: %s", username) + } else { + // 常规场景 + fullUrl = fmt.Sprintf("%s?username=%s&password=%s&mode=verify", + apiUrl, url.QueryEscape(username), url.QueryEscape(password)) + } + + log.Printf("Sending API verification request to: %s", fullUrl) + + // 创建自定义HTTP客户端,跳过SSL证书验证 + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + + // 发送请求到API,使用不验证SSL证书的客户端 + resp, err := client.Get(fullUrl) + if err != nil { + log.Printf("API verification error: %v", err) + return false + } + defer resp.Body.Close() + + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + log.Printf("API verification failed with status: %d", resp.StatusCode) + return false + } + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Failed to read API response: %v", err) + return false + } + log.Printf("API response: %s", string(body)) + + // 解析响应 + var result map[string]interface{} + err = json.NewDecoder(bytes.NewReader(body)).Decode(&result) + if err != nil { + log.Printf("Failed to parse API response: %v", err) + return false + } + + // 检查响应内容 + // 如果响应包含"success",则验证成功 + if status, ok := result["status"].(string); ok && status == "success" { + log.Printf("API verification successful for user: %s", username) + return true + } + + // 如果没有找到预期的成功标识,则验证失败 + log.Printf("API verification failed for user: %s", username) + return false +} \ No newline at end of file diff --git a/cmd/auth/ntlm/ntlm.go b/cmd/auth/ntlm/ntlm.go index 2dd5dd3..04ef92a 100644 --- a/cmd/auth/ntlm/ntlm.go +++ b/cmd/auth/ntlm/ntlm.go @@ -140,13 +140,16 @@ func (c *ntlmContext) authenticate(am *ntlm.AuthenticateMessage, r *auth.NtlmRes } username := am.UserName.String() - password := c.h.Database.GetPassword (username) + log.Printf("NTLM: 尝试验证用户: %s", username) + + password := c.h.Database.GetPassword(username) if password == "" { log.Printf("NTLM: unknown username specified: %s", username) return nil } - c.session.SetUserInfo(username,password,"") + log.Printf("NTLM: 成功获取到用户 %s 的密码", username) + c.session.SetUserInfo(username, password, "") err := c.session.ProcessAuthenticateMessage(am) if err != nil { @@ -156,5 +159,6 @@ func (c *ntlmContext) authenticate(am *ntlm.AuthenticateMessage, r *auth.NtlmRes r.Authenticated = true r.Username = username + log.Printf("NTLM: 用户 %s 认证成功", username) return nil } diff --git a/docs/api-auth.md b/docs/api-auth.md new file mode 100644 index 0000000..e954aa6 --- /dev/null +++ b/docs/api-auth.md @@ -0,0 +1,84 @@ +# NTLM认证API集成 + +RDPGW支持通过API集成NTLM认证,允许使用您自己的用户管理系统进行认证。 + +## API模式 + +API支持两种模式: + +1. **验证模式(verify)**:验证用户凭据是否有效 +2. **密码获取模式(getpassword)**:获取用户的明文密码,用于NTLM挑战-响应计算 + +## API要求 + +### 1. 验证模式 + +用于验证用户凭据是否有效。 + +**请求格式**: +``` +GET https://your-api-server/api/checkperm/?username=<用户名>&password=<密码>&mode=verify +``` + +**成功响应**: +```json +{ + "status": "success" +} +``` + +### 2. 密码获取模式 + +用于获取用户的明文密码,这是NTLM认证所必需的。 + +**请求格式**: +``` +GET https://your-api-server/api/checkperm/?username=<用户名>&mode=getpassword +``` + +**成功响应**: +```json +{ + "status": "success", + "password": "用户的明文密码" +} +``` + +## 配置 + +在`rdpgw-auth.yaml`中配置API认证: + +```yaml +apiauth: + enabled: true + apiurl: "https://your-api-server/api/checkperm/" +``` + +## 安全考虑 + +1. **密码安全**:API必须通过HTTPS提供,并且在内部网络中运行以确保安全性 + +2. **密码存储**:您的API服务必须能够以某种形式获取或计算用户密码,这需要安全的密码存储机制 + +3. **替代方案**:如果不想暴露明文密码,请考虑使用其他认证方式,如OpenID Connect或基本认证 + +## NTLM认证流程 + +1. 客户端(如FreeRDP)发送NTLM协商消息到RDPGW +2. RDPGW生成挑战并发送回客户端 +3. 客户端计算响应并发送认证消息 +4. RDPGW调用API以`getpassword`模式获取用户密码 +5. RDPGW使用获取的密码计算期望的响应并与客户端响应比较 +6. 如果匹配,认证成功;否则,认证失败 + +## 优势 + +- 保持标准NTLM认证流程,客户端无需修改 +- 与您的用户管理系统集成 +- 支持所有NTLM客户端,包括标准Windows远程桌面客户端 + +## 注意事项 + +- 您的API必须能够安全地存储和提供用户密码 +- 明文密码传输存在固有的安全风险,即使在加密通道中也是如此 +- 确保API服务器仅对RDPGW服务器可访问,并考虑实施IP限制或类似措施 \ No newline at end of file diff --git a/docs/rdpgw-auth-api.yaml.example b/docs/rdpgw-auth-api.yaml.example new file mode 100644 index 0000000..290108d --- /dev/null +++ b/docs/rdpgw-auth-api.yaml.example @@ -0,0 +1,23 @@ +# RDPGW认证配置示例 +# 将此文件保存为rdpgw-auth.yaml并根据您的环境调整设置 + +# API认证配置 +ApiAuth: + # 启用API认证 + enabled: true + # API URL - 验证用户凭据的端点 + # NTLM认证需要API提供明文密码 + # 此API需要支持以下两种模式: + # 1. ?username=user&mode=getpassword - 返回用户的明文密码 + # 2. ?username=user&password=pass&mode=verify - 验证凭据是否正确 + apiurl: "https://your-api-server/api/checkperm/" + +# 用户配置 +# 当使用API认证时,此部分可以为空 +# 如果为特定用户提供密码,将绕过API并使用本地密码 +Users: [] + +# 或者,您也可以同时支持API认证和本地用户认证 +# 在这种情况下,请提供本地用户凭据 +# users: +# - {Username: "local_user", Password: "local_password"} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b2a41d4 --- /dev/null +++ b/index.js @@ -0,0 +1,132 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); + +// 配置信息 +const config = { + port: 3000, + // 用户信息,可以通过配置文件覆盖 + users: { + 'testuser': 'testpassword', + 'admin': 'adminpass', + 'user1': 'password1' + }, + // API路径 + apiPath: '/api/checkperm' +}; + +// 尝试加载配置文件 +try { + const configPath = path.join(__dirname, 'config.json'); + if (fs.existsSync(configPath)) { + const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); + // 合并配置 + Object.assign(config, fileConfig); + console.log('已加载配置文件'); + } +} catch (error) { + console.log('加载配置文件失败,使用默认配置:', error.message); +} + +const app = express(); + +// 添加中间件解析JSON和表单数据 +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// 处理认证逻辑的函数 +function handleAuth(username, password, mode) { + // 验证参数是否存在 + if (!username) { + console.log('缺少必要参数: username'); + return { status: 400, response: { status: 'error', message: '缺少必要参数: username' } }; + } + + // 检查用户是否存在 + if (!config.users[username]) { + console.log(`用户不存在: ${username}`); + return { status: 401, response: { status: 'error', message: '用户不存在' } }; + } + + // 根据模式处理请求 + if (mode === 'getpassword') { + // 密码获取模式 - 用于NTLM认证 + console.log(`返回用户 ${username} 的密码`); + return { + status: 200, + response: { + status: 'success', + password: config.users[username] + } + }; + } else { + // 默认为verify模式 - 验证用户名和密码 + if (!password) { + console.log('缺少必要参数: password'); + return { status: 400, response: { status: 'error', message: '缺少必要参数: password' } }; + } + + if (config.users[username] === password) { + console.log('认证成功'); + return { + status: 200, + response: { + status: 'success', + user: username + } + }; + } else { + console.log('认证失败: 密码不正确'); + return { status: 401, response: { status: 'error', message: '认证失败' } }; + } + } +} + +// 认证API端点 - GET +app.get(config.apiPath, (req, res) => { + const { username, password, mode } = req.query; + + console.log('收到GET认证请求:'); + console.log(`username: ${username}`); + console.log(`mode: ${mode || 'verify'}`); + if (password) { + console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码 + } + + const result = handleAuth(username, password, mode); + return res.status(result.status).json(result.response); +}); + +// 认证API端点 - POST +app.post(config.apiPath, (req, res) => { + const { username, password, mode } = req.body; + + console.log('收到POST认证请求:'); + console.log(`username: ${username}`); + console.log(`mode: ${mode || 'verify'}`); + if (password) { + console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码 + } + + const result = handleAuth(username, password, mode); + return res.status(result.status).json(result.response); +}); + +// 根路径返回服务信息 +app.get('/', (req, res) => { + res.send('RDPGW远程认证测试服务已启动'); +}); + +// 启动服务器 +app.listen(config.port, () => { + console.log(`认证服务器已启动,监听端口: ${config.port}`); + console.log('当前配置:'); + console.log(`- 端口: ${config.port}`); + console.log(`- API路径: ${config.apiPath}`); + console.log(`- 已配置用户数: ${Object.keys(config.users).length}`); + console.log('\n支持的模式:'); + console.log(`1. 验证模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&password=testpassword&mode=verify`); + console.log(`2. 密码获取模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&mode=getpassword`); + console.log('---'); + console.log('POST请求也支持,可以通过请求体发送参数'); +}); \ No newline at end of file