这篇文章我们讲解《网关认证配置服务》,主要涉及网关配置服务、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的集成,网关能够自动感知服务实例的变化,动态调整路由目标,极大增强了微服务架构下的弹性和容错能力。