43.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成认证(二)

发布于:2025-08-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

这篇文章我们讲解《网关认证配置服务》,主要涉及网关配置服务Nacos服务发现服务基于Nacos的网关配置服务。废话不多说我们开始吧。

一、网关配置服务

在微服务架构中,网关作为系统的入口,承担着请求路由、认证鉴权等重要职责。为了实现灵活的认证策略和便捷的配置管理,我们需要设计一套网关配置服务。该服务主要负责以下几个方面:

  • 认证配置:集中管理与认证相关的配置信息,如身份服务的客户端ID、密钥、超时时间等,便于后续统一维护和动态调整。
  • 跳过认证的路径:有些接口(如健康检查、静态资源等)无需认证即可访问,配置服务需支持动态维护这些路径列表。
  • 需要认证的路径:对外开放的业务接口通常需要认证,配置服务需明确哪些路径必须经过身份验证。
  • 身份服务配置:网关需要与身份服务(如OAuth2、OpenID Connect等)集成,相关的服务地址、认证参数等也应由配置服务统一管理。

本节我们将首先通过接口定义网关配置服务的核心功能,为后续的具体实现和集成打下基础。这样做不仅提升了系统的可维护性,也为后续支持动态配置和服务发现提供了良好的扩展点。

首先,我们在网关服务的Models目录下创建身份服务配置类IdentityServiceConfig,代码如下,这里就不多讲了。

/// <summary>
/// 身份服务配置
/// </summary>
public class IdentityServiceConfig
{
    /// <summary>
    /// 客户端ID
    /// </summary>
    public string ClientId { get; set; } = "gateway-client";
    
    /// <summary>
    /// 客户端密钥
    /// </summary>
    public string ClientSecret { get; set; } = "gateway-secret";
    
    /// <summary>
    /// 超时时间(秒)
    /// </summary>
    public int TimeoutSeconds { get; set; } = 30;
    
    /// <summary>
    /// 重试次数
    /// </summary>
    public int RetryCount { get; set; } = 3;
}

接下来,我们需要为网关服务设计一个统一的配置接口,以便后续灵活地扩展和维护网关的认证相关配置。为此,我们将在Services目录下创建一个名为IGatewayConfigService的接口,下面是接口的代码定义:

using SP.Gateway.Models;

namespace SP.Gateway.Services;

/// <summary>
/// 网关配置服务接口
/// </summary>
public interface IGatewayConfigService
{
    /// <summary>
    /// 获取跳过认证的路径
    /// </summary>
    /// <returns>跳过认证的路径列表</returns>
    Task<List<string>> GetSkipAuthenticationPathsAsync();
    
    /// <summary>
    /// 获取需要认证的路径
    /// </summary>
    /// <returns>需要认证的路径列表</returns>
    Task<List<string>> GetRequireAuthenticationPathsAsync();
    
    /// <summary>
    /// 检查路径是否需要认证
    /// </summary>
    /// <param name="path">请求路径</param>
    /// <returns>是否需要认证</returns>
    Task<bool> IsAuthenticationRequiredAsync(string path);
    
    /// <summary>
    /// 获取身份服务配置
    /// </summary>
    /// <returns>身份服务配置</returns>
    Task<IdentityServiceConfig> GetIdentityServiceConfigAsync();
}

通过定义IGatewayConfigService接口,我们为网关服务的认证配置提供了统一的入口,既方便了后续的具体实现,也为动态配置和服务发现等功能的集成打下了坚实的基础。

二、Nacos服务发现服务

在微服务架构中,服务发现是实现服务动态注册与查找的关键机制。Nacos作为一款优秀的服务发现与配置管理平台,能够帮助我们高效地管理各个微服务的实例信息。为了让网关能够动态感知身份服务的可用实例,我们需要设计并实现一个Nacos服务发现服务。

首先,定义一个INacosServiceDiscoveryService接口,约定了服务发现的核心功能,包括获取身份服务实例列表、选择最佳服务实例以及健康检查等。该接口为网关与Nacos集成提供了统一的抽象层,便于后续扩展和维护。接口代码如下:

namespace SP.Gateway.Services;

/// <summary>
/// Nacos服务发现服务接口
/// </summary>
public interface INacosServiceDiscoveryService
{
    /// <summary>
    /// 获取身份服务实例列表
    /// </summary>
    /// <returns>身份服务实例列表</returns>
    Task<List<string>> GetIdentityServiceUrlsAsync();
    
    /// <summary>
    /// 获取最佳的身份服务URL
    /// </summary>
    /// <returns>最佳URL</returns>
    Task<string?> GetBestIdentityServiceUrlAsync();
    
    /// <summary>
    /// 检查服务是否可用
    /// </summary>
    /// <param name="url">服务URL</param>
    /// <returns>是否可用</returns>
    Task<bool> IsServiceAvailableAsync(string url);
}

接下来,在Impl文件夹下实现NacosServiceDiscoveryService类,具体负责与Nacos进行交互,动态获取身份服务的可用实例,并通过健康检查机制筛选出可用的服务URL。实现代码如下:

using Nacos.V2;

namespace SP.Gateway.Services.Impl;

/// <summary>
/// Nacos服务发现服务实现
/// </summary>
public class NacosServiceDiscoveryService : INacosServiceDiscoveryService
{
    private readonly INacosNamingService _namingService;
    private readonly HttpClient _httpClient;
    private readonly ILogger<NacosServiceDiscoveryService> _logger;
    private readonly Dictionary<string, DateTime> _lastHealthCheck = new();
    private readonly Dictionary<string, bool> _healthStatus = new();
    private readonly object _lockObject = new();
    private const string IdentityServiceName = "SPIdentityService";

    public NacosServiceDiscoveryService(
        INacosNamingService namingService,
        HttpClient httpClient,
        ILogger<NacosServiceDiscoveryService> logger)
    {
        _namingService = namingService;
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<List<string>> GetIdentityServiceUrlsAsync()
    {
        try
        {
            var instances = await _namingService.SelectInstances(IdentityServiceName, "DEFAULT_GROUP", new List<string> { "DEFAULT" }, true);
            
            if (instances == null || !instances.Any())
            {
                _logger.LogWarning("从Nacos获取身份服务实例失败或没有可用实例");
                return new List<string>();
            }

            var urls = new List<string>();
            foreach (var instance in instances)
            {
                if (instance.Enabled && instance.Healthy)
                {
                    var scheme = instance.Metadata?.GetValueOrDefault("scheme", "http");
                    var url = $"{scheme}://{instance.Ip}:{instance.Port}";
                    urls.Add(url);
                }
            }

            _logger.LogDebug("从Nacos获取到 {Count} 个身份服务实例", urls.Count);
            return urls;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "从Nacos获取身份服务实例时发生错误");
            return new List<string>();
        }
    }

    public async Task<string?> GetBestIdentityServiceUrlAsync()
    {
        var urls = await GetIdentityServiceUrlsAsync();
        
        if (!urls.Any())
        {
            _logger.LogWarning("没有可用的身份服务实例");
            return null;
        }

        var availableUrls = new List<string>();
        foreach (var url in urls)
        {
            if (await IsServiceAvailableAsync(url))
            {
                availableUrls.Add(url);
            }
        }

        if (!availableUrls.Any())
        {
            _logger.LogWarning("所有身份服务实例都不可用");
            return null;
        }

        var random = new Random();
        var selectedUrl = availableUrls[random.Next(availableUrls.Count)];
        
        _logger.LogDebug("选择身份服务URL: {Url}", selectedUrl);
        return selectedUrl;
    }

    public async Task<bool> IsServiceAvailableAsync(string url)
    {
        lock (_lockObject)
        {
            if (_lastHealthCheck.TryGetValue(url, out var lastCheck))
            {
                var timeSinceLastCheck = DateTime.UtcNow - lastCheck;
                if (timeSinceLastCheck.TotalSeconds < 5)
                {
                    return _healthStatus.GetValueOrDefault(url, false);
                }
            }
        }

        try
        {
            var discoveryUrl = $"{url.TrimEnd('/')}/.well-known/openid_configuration";
            var response = await _httpClient.GetAsync(discoveryUrl, CancellationToken.None);
            
            var isHealthy = response.IsSuccessStatusCode;
            
            lock (_lockObject)
            {
                _healthStatus[url] = isHealthy;
                _lastHealthCheck[url] = DateTime.UtcNow;
            }
            
            if (isHealthy)
            {
                _logger.LogDebug("身份服务 {Url} 健康检查通过", url);
            }
            else
            {
                _logger.LogWarning("身份服务 {Url} 健康检查失败,状态码: {StatusCode}", url, response.StatusCode);
            }
            
            return isHealthy;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "检查身份服务 {Url} 健康状态时发生错误", url);
            
            lock (_lockObject)
            {
                _healthStatus[url] = false;
                _lastHealthCheck[url] = DateTime.UtcNow;
            }
            
            return false;
        }
    }
}

这段代码通过集成Nacos的INacosNamingService接口,实现了身份服务实例的动态发现与管理。每当网关需要访问身份服务时,都会调用GetIdentityServiceUrlsAsync方法从Nacos注册中心获取当前可用的服务实例列表。为了保证服务的高可用性,代码还实现了健康检查机制:在IsServiceAvailableAsync方法中,会定期向每个服务实例的/.well-known/openid_configuration端点发起HTTP请求,判断服务是否健康。健康检查结果会被缓存,避免对同一实例进行过于频繁的检查,从而有效降低了网络和计算资源的消耗。

在选择最佳服务实例时,GetBestIdentityServiceUrlAsync方法会优先筛选出健康的服务节点,并采用随机算法进行负载均衡分发,进一步提升了系统的可用性和扩展性。通过这种方式,网关能够自动感知服务实例的变化,动态调整路由目标,极大增强了微服务架构下的弹性和容错能力。

三、基于Nacos的网关配置服务

在微服务架构下,网关的配置管理需要支持动态性和高可用性。为此,我们实现了一个基于Nacos的网关配置服务NacosGatewayConfigService,它通过Nacos服务发现和配置中心,动态获取网关认证相关的配置信息,并具备本地缓存和容错能力。该服务实现了IGatewayConfigService接口,支持获取跳过认证的路径、需要认证的路径、身份服务配置等功能。

首先,服务会优先尝试从Nacos注册中心发现配置服务实例,并通过HTTP接口获取最新的网关配置内容(如跳过认证路径、需要认证路径、身份服务参数等),如果Nacos不可用则回退到本地配置。为提升性能和稳定性,所有配置项都采用内存缓存,定期刷新,避免频繁访问远程服务。健康检查和异常处理机制确保即使Nacos或配置服务临时不可用,网关依然可以使用最近一次的有效配置,保障系统的高可用性。

具体实现代码如下:

using System.Text.Json;
using Nacos.V2;
using SP.Gateway.Models;

namespace SP.Gateway.Services.Impl;

/// <summary>
/// 基于Nacos的网关配置服务实现
/// </summary>
public class NacosGatewayConfigService : IGatewayConfigService
{
    private readonly INacosNamingService _namingService;
    private readonly IConfiguration _configuration;
    private readonly ILogger<NacosGatewayConfigService> _logger;
    private readonly Dictionary<string, DateTime> _lastConfigCheck = new();
    private readonly Dictionary<string, object> _configCache = new();
    private readonly object _lockObject = new();
    private const string ConfigServiceName = "SPConfigService";
    private const int ConfigCacheMinutes = 5;

    public NacosGatewayConfigService(
        INacosNamingService namingService,
        IConfiguration configuration,
        ILogger<NacosGatewayConfigService> logger)
    {
        _namingService = namingService;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task<List<string>> GetSkipAuthenticationPathsAsync()
    {
        var cacheKey = "skip_authentication_paths";
        var cached = GetCachedConfig<List<string>>(cacheKey);
        if (cached != null)
        {
            return cached;
        }

        try
        {
            var configValue = await GetConfigFromNacosAsync("Gateway:SkipAuthenticationPaths");
            
            if (string.IsNullOrEmpty(configValue))
            {
                var defaultPaths = new List<string>
                {
                    "/swagger",
                    "/api/auth",
                    "/health",
                    "/favicon.ico",
                    "/.well-known"
                };
                
                SetCachedConfig(cacheKey, defaultPaths);
                return defaultPaths;
            }

            var paths = JsonSerializer.Deserialize<List<string>>(configValue);
            SetCachedConfig(cacheKey, paths);
            return paths ?? new List<string>();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取跳过认证路径时发生错误");
            return new List<string>();
        }
    }

    public async Task<List<string>> GetRequireAuthenticationPathsAsync()
    {
        var cacheKey = "require_authentication_paths";
        var cached = GetCachedConfig<List<string>>(cacheKey);
        if (cached != null)
        {
            return cached;
        }

        try
        {
            var configValue = await GetConfigFromNacosAsync("Gateway:RequireAuthenticationPaths");
            
            if (string.IsNullOrEmpty(configValue))
            {
                var defaultPaths = new List<string>
                {
                    "/api/finance",
                    "/api/currency",
                    "/api/config",
                    "/api/report"
                };
                
                SetCachedConfig(cacheKey, defaultPaths);
                return defaultPaths;
            }

            var paths = JsonSerializer.Deserialize<List<string>>(configValue);
            SetCachedConfig(cacheKey, paths);
            return paths ?? new List<string>();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取需要认证路径时发生错误");
            return new List<string>();
        }
    }

    public async Task<bool> IsAuthenticationRequiredAsync(string path)
    {
        var skipPaths = await GetSkipAuthenticationPathsAsync();
        if (skipPaths.Any(skipPath => path.StartsWith(skipPath, StringComparison.OrdinalIgnoreCase)))
        {
            return false;
        }

        var requirePaths = await GetRequireAuthenticationPathsAsync();
        if (requirePaths.Any(requirePath => path.StartsWith(requirePath, StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }

        return true;
    }

    public async Task<IdentityServiceConfig> GetIdentityServiceConfigAsync()
    {
        var cacheKey = "identity_service_config";
        var cached = GetCachedConfig<IdentityServiceConfig>(cacheKey);
        if (cached != null)
        {
            return cached;
        }

        try
        {
            var configValue = await GetConfigFromNacosAsync("Gateway:IdentityService");
            
            if (string.IsNullOrEmpty(configValue))
            {
                var defaultConfig = new IdentityServiceConfig();
                SetCachedConfig(cacheKey, defaultConfig);
                return defaultConfig;
            }

            var config = JsonSerializer.Deserialize<IdentityServiceConfig>(configValue);
            SetCachedConfig(cacheKey, config);
            return config ?? new IdentityServiceConfig();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取身份服务配置时发生错误");
            return new IdentityServiceConfig();
        }
    }

    private async Task<string?> GetConfigFromNacosAsync(string configKey)
    {
        try
        {
            var configServiceUrl = await GetConfigServiceUrlAsync();
            if (!string.IsNullOrEmpty(configServiceUrl))
            {
                using var httpClient = new HttpClient();
                var response = await httpClient.GetAsync($"{configServiceUrl}/api/config/{configKey}");
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsStringAsync();
                }
            }
            
            return _configuration[configKey];
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "从Nacos获取配置 {ConfigKey} 时发生错误", configKey);
            return null;
        }
    }

    private async Task<string?> GetConfigServiceUrlAsync()
    {
        try
        {
            var instances = await _namingService.SelectInstances(ConfigServiceName, "DEFAULT_GROUP", new List<string> { "DEFAULT" }, true);
            if (instances?.Any() == true)
            {
                var instance = instances.First(h => h.Enabled && h.Healthy);
                var scheme = instance.Metadata?.GetValueOrDefault("scheme", "http");
                return $"{scheme}://{instance.Ip}:{instance.Port}";
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取配置服务URL时发生错误");
        }
        
        return null;
    }

    private T? GetCachedConfig<T>(string key)
    {
        lock (_lockObject)
        {
            if (_configCache.TryGetValue(key, out var cachedValue))
            {
                if (_lastConfigCheck.TryGetValue(key, out var lastCheck))
                {
                    var timeSinceLastCheck = DateTime.UtcNow - lastCheck;
                    if (timeSinceLastCheck.TotalMinutes < ConfigCacheMinutes)
                    {
                        return (T)cachedValue;
                    }
                }
            }
        }
        
        return default(T);
    }

    private void SetCachedConfig<T>(string key, T value)
    {
        lock (_lockObject)
        {
            _configCache[key] = value!;
            _lastConfigCheck[key] = DateTime.UtcNow;
        }
    }
}

通过这种方式,网关的认证配置实现了集中化和动态化管理。无论是跳过认证的路径、需要认证的路径,还是身份服务的参数,都可以在Nacos平台统一配置和实时调整。即使Nacos或配置服务临时不可用,系统也能依赖本地缓存继续稳定运行,保障了高可用性和灵活性。

四、总结

 return (T)cachedValue;
                }
            }
        }
    }
    
    return default(T);
}

private void SetCachedConfig<T>(string key, T value)
{
    lock (_lockObject)
    {
        _configCache[key] = value!;
        _lastConfigCheck[key] = DateTime.UtcNow;
    }
}

}

通过这种方式,网关的认证配置实现了集中化和动态化管理。无论是跳过认证的路径、需要认证的路径,还是身份服务的参数,都可以在Nacos平台统一配置和实时调整。即使Nacos或配置服务临时不可用,系统也能依赖本地缓存继续稳定运行,保障了高可用性和灵活性。

### 四、总结
在本文中,我们实现了网关认证配置服务,主要包括网关配置服务、Nacos服务发现服务和基于Nacos的网关配置服务。通过这些服务,网关能够动态获取认证相关的配置信息,并具备高可用性和容错能力。这种设计不仅提升了系统的可维护性,也为后续支持动态配置和服务发现等功能提供了良好的扩展点。通过与Nacos的集成,网关能够自动感知服务实例的变化,动态调整路由目标,极大增强了微服务架构下的弹性和容错能力。

网站公告

今日签到

点亮在社区的每一天
去签到