lvfengfree 9e3b1f3c03 feat: 添加远程桌面Token分享功能
- 新增 WindowsCredential 模型和控制器,用于管理 Windows 凭据
- 新增 RemoteAccessToken 模型,支持生成可分享的远程访问链接
- 更新 RemoteDesktopController,添加 Token 生成、验证、撤销等 API
- 更新前端 RemoteDesktopModal,支持4种连接方式:快速连接、生成分享链接、手动输入、链接管理
- 新增 WindowsCredentialManager 组件用于管理 Windows 凭据
- 新增 RemoteAccessPage 用于通过 Token 访问远程桌面
- 添加 Vue Router 支持 /remote/:token 路由
- 更新数据库迁移,添加 WindowsCredentials 和 RemoteAccessTokens 表
2026-01-20 15:00:44 +08:00

151 lines
9.0 KiB
C#

using AmtScanner.Api.Data;
using AmtScanner.Api.Models;
using AmtScanner.Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
namespace AmtScanner.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RemoteDesktopController : ControllerBase
{
private readonly IGuacamoleService _guacamoleService;
private readonly AppDbContext _context;
private readonly ILogger<RemoteDesktopController> _logger;
public RemoteDesktopController(IGuacamoleService guacamoleService, AppDbContext context, ILogger<RemoteDesktopController> logger)
{
_guacamoleService = guacamoleService;
_context = context;
_logger = logger;
}
[HttpPost("generate-token/{deviceId}")]
public ActionResult GenerateToken(long deviceId, [FromBody] GenerateTokenRequest request)
{
return Ok(new { success = true, deviceId = deviceId, minutes = request.ExpiresInMinutes });
// var device = await _context.AmtDevices.FindAsync(deviceId);
if (device == null) return NotFound(new { error = "设备不存在" });
WindowsCredential? credential = request.CredentialId.HasValue
? await _context.WindowsCredentials.FindAsync(request.CredentialId.Value)
: await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault);
if (credential == null) return BadRequest(new { error = "请先配置 Windows 凭据" });
var token = GenerateRandomToken();
var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30);
var accessToken = new RemoteAccessToken { Token = token, DeviceId = deviceId, WindowsCredentialId = credential.Id, ExpiresAt = expiresAt, MaxUseCount = request.MaxUseCount ?? 1, Note = request.Note };
_context.RemoteAccessTokens.Add(accessToken);
await _context.SaveChangesAsync();
var baseUrl = $"{Request.Scheme}://{Request.Host}";
return Ok(new GenerateTokenResponse { Success = true, Token = token, AccessUrl = $"{baseUrl}/remote/{token}", ExpiresAt = expiresAt, MaxUseCount = accessToken.MaxUseCount, DeviceIp = device.IpAddress });
}
[HttpGet("connect-by-token/{token}")]
public async Task<ActionResult<RemoteDesktopResponse>> ConnectByToken(string token)
{
var accessToken = await _context.RemoteAccessTokens.Include(t => t.Device).Include(t => t.WindowsCredential).FirstOrDefaultAsync(t => t.Token == token);
if (accessToken == null) return NotFound(new { error = "无效的访问链接" });
if (!accessToken.IsValid()) return BadRequest(new { error = "访问链接已过期或已达到使用次数上限" });
if (accessToken.Device == null || accessToken.WindowsCredential == null) return BadRequest(new { error = "设备或凭据信息不完整" });
accessToken.UseCount++;
accessToken.UsedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
var guacToken = await _guacamoleService.GetAuthTokenAsync();
if (string.IsNullOrEmpty(guacToken)) return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(guacToken, $"AMT-{accessToken.Device.IpAddress}", accessToken.Device.IpAddress, accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password);
if (string.IsNullOrEmpty(connectionId)) return StatusCode(500, new { error = "创建远程连接失败" });
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
return Ok(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken, DeviceIp = accessToken.Device.IpAddress });
}
[HttpGet("validate-token/{token}")]
public async Task<ActionResult<ValidateTokenResponse>> ValidateToken(string token)
{
var accessToken = await _context.RemoteAccessTokens.Include(t => t.Device).FirstOrDefaultAsync(t => t.Token == token);
if (accessToken == null) return Ok(new ValidateTokenResponse { Valid = false, Error = "无效的访问链接" });
if (!accessToken.IsValid()) return Ok(new ValidateTokenResponse { Valid = false, Error = "访问链接已过期或已达到使用次数上限" });
return Ok(new ValidateTokenResponse { Valid = true, DeviceIp = accessToken.Device?.IpAddress, ExpiresAt = accessToken.ExpiresAt, RemainingUses = accessToken.MaxUseCount > 0 ? accessToken.MaxUseCount - accessToken.UseCount : -1 });
}
[HttpGet("list-tokens/{deviceId}")]
public async Task<ActionResult<IEnumerable<TokenInfoDto>>> GetDeviceTokens(long deviceId)
{
var tokens = await _context.RemoteAccessTokens.Where(t => t.DeviceId == deviceId && t.ExpiresAt > DateTime.UtcNow).OrderByDescending(t => t.CreatedAt)
.Select(t => new TokenInfoDto { Id = t.Id, Token = t.Token, CreatedAt = t.CreatedAt, ExpiresAt = t.ExpiresAt, MaxUseCount = t.MaxUseCount, UseCount = t.UseCount, Note = t.Note }).ToListAsync();
return Ok(tokens);
}
[HttpDelete("revoke-token/{tokenId}")]
public async Task<ActionResult> RevokeToken(long tokenId)
{
var token = await _context.RemoteAccessTokens.FindAsync(tokenId);
if (token == null) return NotFound(new { error = "Token 不存在" });
_context.RemoteAccessTokens.Remove(token);
await _context.SaveChangesAsync();
return Ok(new { success = true });
}
[HttpPost("cleanup-tokens")]
public async Task<ActionResult> CleanupExpiredTokens()
{
var count = await _context.RemoteAccessTokens.Where(t => t.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync();
return Ok(new { success = true, deletedCount = count });
}
[HttpPost("connect/{deviceId}")]
public async Task<ActionResult<RemoteDesktopResponse>> Connect(long deviceId, [FromBody] RdpCredentials credentials)
{
var device = await _context.AmtDevices.FindAsync(deviceId);
if (device == null) return NotFound(new { error = "设备不存在" });
var guacToken = await _guacamoleService.GetAuthTokenAsync();
if (string.IsNullOrEmpty(guacToken)) return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(guacToken, $"AMT-{device.IpAddress}", device.IpAddress, credentials.Username, credentials.Password);
if (string.IsNullOrEmpty(connectionId)) return StatusCode(500, new { error = "创建远程连接失败" });
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
return Ok(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken });
}
[HttpPost("test-post/{id}")]
public ActionResult TestPost(long id, [FromBody] GenerateTokenRequest request)
{
return Ok(new { success = true, id = id, minutes = request.ExpiresInMinutes });
}
[HttpGet("test")]
public async Task<ActionResult> TestConnection()
{
var token = await _guacamoleService.GetAuthTokenAsync();
if (string.IsNullOrEmpty(token)) return StatusCode(503, new { success = false, error = "无法连接到 Guacamole 服务" });
return Ok(new { success = true, message = "Guacamole 服务连接正常" });
}
private static string GenerateRandomToken()
{
var bytes = new byte[24];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").Replace("=", "");
}
}
public class GenerateTokenRequest { public long? CredentialId { get; set; } public int? ExpiresInMinutes { get; set; } = 30; public int? MaxUseCount { get; set; } = 1; public string? Note { get; set; } }
public class GenerateTokenResponse { public bool Success { get; set; } public string Token { get; set; } = ""; public string AccessUrl { get; set; } = ""; public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } }
public class ValidateTokenResponse { public bool Valid { get; set; } public string? DeviceIp { get; set; } public DateTime? ExpiresAt { get; set; } public int RemainingUses { get; set; } public string? Error { get; set; } }
public class TokenInfoDto { public long Id { get; set; } public string Token { get; set; } = ""; public DateTime CreatedAt { get; set; } public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public int UseCount { get; set; } public string? Note { get; set; } }
public class RdpCredentials { public string Username { get; set; } = ""; public string Password { get; set; } = ""; }
public class RemoteDesktopResponse { public bool Success { get; set; } public string? ConnectionUrl { get; set; } public string? ConnectionId { get; set; } public string? Token { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } }