点击关注上方蓝字,阅读更多干货~
缦图导读
在当今复杂的微服务环境中,Spring Cloud Gateway 扮演着至关重要的角色。本文将深入剖析该组件,不仅提供对其概念、架构演进和核心功能的全面了解,更注重实用性。我们致力于帮助读者更有效地构建安全、高效的API网关,期望本文能为读者提供实际帮助和启发,使其在实践中取得显著成果。
1 前言
1 背景
随着互联网的快速发展,微服务架构逐渐成为现代应用程序的主流设计模式。微服务架构将应用程序拆分成一系列小型、独立的服务,每个服务都具有特定的功能。这种架构使得应用程序更加模块化、可扩展性和易于维护。然而,随着微服务数量的增加,如何有效地管理和路由请求成为了一个重要的问题。
微服务网关作为微服务架构中的重要组件,负责处理客户端请求,将请求路由到正确的服务,并将响应返回给客户端。本文的探讨将以微服务架构下网关的概述、架构演进、核心功能等方面进行展开。
Spring Cloud Gateway 的发展始于对传统 Servlet 的网关(如 Zuul)的一些认识。在过去,Zuul 作为 Netflix OSS 的一部分,虽然能够胜任一些基本的网关功能,但在高并发和大规模微服务架构中遇到了一些性能瓶颈和不足之处。
随着Spring Framework的发展和Spring Cloud项目的兴起,Spring Cloud Gateway 于2018年发布,它利用了 Spring 5、Project Reactor 和 Spring Boot 2 等新技术,采用了反应式编程模型,从而更好地满足了云原生和微服务架构的需求。
综合考虑这些因素,选择 Spring Cloud Gateway 可以为构建弹性、高性能、可扩展的 API 网关提供一种现代、全面的解决方案。
在我们早期的架构中,每个集群只配置了一个网关,同时负责处理内外网流量的转发。这样的设计虽然维护成本低,但也引发了一系列复杂性问题,其中一个主要问题是内外网流量的处理存在明显差异。
外网流量通常要求严格的鉴权,而内网流量则无需经过鉴权即可访问。这不仅牵扯到接口权限控制的不同,还涉及到某些服务调用并没有鉴权所需的token,使其访问其他集群服务则变得十分繁琐。
图2 内外双网关体系
动态路由:实现灵活的请求路由管理。
统一鉴权:对外网流量进行严格的统一鉴权,确保安全性。
自定义请求 Header:支持个性化的请求 Header 配置。
跨域访问:处理跨域请求,提升系统的可访问性。
请求日志:记录外网流量的请求日志,用于监控和分析。
验签:对外部请求进行验证,确保数据完整性。
动态路由:管理内网流量的动态路由。
跨集群内网调用:支持内网集群间的灵活调用。
通过这样的设计,可以更好地平衡内外网流量的需求,提高系统的安全性、性能和灵活性,使整个架构更为健壮和可维护。
图3 动态路由示意图
为什么需要动态路由?
在微服务架构中,服务实例的动态性是常见的情况。新服务的部署、服务的上下线,以及负载均衡策略的变更都可能导致服务实例的变化。为了应对这种动态性,网关需要能够在运行时动态地调整路由规则,以确保请求可以正确地路由到可用的服务实例。
动态路由是什么?它的优势是什么?
动态路由是指在运行时根据配置或其他条件调整请求的路由方式。与静态路由相对,动态路由能够根据系统的实时状态和配置的变更来动态地调整请求的转发规则。它的优势有以下几点:
适应性强:允许根据服务的变化自动调整路由规则,提高系统的适应性。
灵活性高:可以根据实际需求,动态地更改请求的路由策略,而无需停机或手动配置。
支持负载均衡:能够自动实现负载均衡,确保请求被均匀分发到各个可用的服务实例上,提高系统的性能和可伸缩性。
Spring Cloud Gateway 如何实现自动路由的?
Spring Cloud Gateway 的自动路由原理主要基于 Spring Cloud 的服务发现机制。当 Spring Cloud Gateway 与 Nacos(或其他支持服务发现的注册中心)集成时,它会通过以下步骤实现自动路由:
通过以上步骤,Spring Cloud Gateway 通过与 Nacos 的集成,利用 Nacos 提供的服务发现和注册功能,动态地管理路由规则。这种自动路由的机制使得网关能够更灵活地适应微服务架构中服务实例的动态性变化,提高了系统的可维护性和弹性。
统一鉴权是什么?它的优势是什么?
统一鉴权是指在分布式系统中,通过一致的身份验证和授权机制,确保对系统内各个服务的访问都经过相同的鉴权验证。这意味着无论请求到达哪个服务,都要经过同一套鉴权逻辑,以确保安全性和一致性。它的优势可以分为以下几点:
一致性:统一鉴权确保了在整个系统中应用相同的鉴权规则,避免了各个服务各自实现鉴权逻辑,导致不一致的问题。
简化管理:维护一套统一的鉴权逻辑可以简化系统管理和维护的工作,降低了复杂性。
public class AuthFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 在这里执行鉴权逻辑,例如验证JWT token、检查用户权限等
// 如果鉴权失败,可以返回错误响应
if (!authenticationPassed) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 鉴权通过,继续执行后续过滤器和路由
return chain.filter(exchange);
}
public int getOrder() {
// 设置过滤器的执行顺序,数字越小越先执行
return Ordered.HIGHEST_PRECEDENCE;
}
}
图4 鉴权流程(简化版)
业务背景
减轻下游服务负担:在网关层进行一次身份验证或者信息提取,可以减轻下游服务的负担,使得服务更专注于业务逻辑。
具体实现
在 Spring Cloud Gateway 中,可以通过添加全局的过滤器来实现自定义请求头的注入。以下是一个简单的示例,演示如何在网关层添加用户ID到请求头:
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomHeaderFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 在这里获取用户ID等信息,可以从认证信息、会话信息中提取
String userId = getUserIdFromAuthentication();
// 将用户ID添加到请求头
exchange.getRequest().mutate().header("X-User-ID", userId);
// 继续执行后续过滤器和路由
return chain.filter(exchange);
}
public int getOrder() {
// 设置过滤器的执行顺序,数字越小越先执行
return Ordered.HIGHEST_PRECEDENCE;
}
// 示例方法,从认证信息中获取用户ID
private String getUserIdFromAuthentication() {
// 实际应用中,可以根据具体的认证机制来获取用户ID
// 这里仅作为演示,实际应根据业务需求进行实现
return "123456";
}
}
在这个例子中,CustomHeaderFilter 类实现了 GlobalFilter 接口,通过重写 filter 方法添加了自定义请求头。在这里,你可以根据业务逻辑获取用户ID等信息,然后将其添加到请求头中。
请注意,这只是一个简单的示例,实际的业务中可能需要更复杂的逻辑,例如从认证信息中提取用户ID、规避非法传递标准头问题等;
跨域访问是什么?
跨域访问是指在 Web 应用中,一个域下的页面请求了另一个域下的资源。浏览器出于安全考虑,使用同源策略(Same-Origin Policy)来限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。同源策略要求页面的协议、端口和域名必须相同,否则浏览器将阻止跨域请求。
Spring Cloud Gateway 怎么实现跨域访问?
Spring Cloud Gateway 作为一个 API 网关,可以通过配置来处理跨域请求。以下是一个简单的配置示例,展示了如何在 Spring Cloud Gateway 中实现跨域访问:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
public class CorsConfig {
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOrigin("*"); // 允许所有来源
corsConfig.addAllowedMethod("*"); // 允许所有请求方法
corsConfig.addAllowedHeader("*"); // 允许所有请求头
corsConfig.setAllowCredentials(true); // 允许发送 Cookie
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
}
setAllowCredentials:设置是否允许发送 Cookie。
业务背景
在我们内部也产生了一场请求日志到底是由各个业务端自行去打印还是由网关统一去打印的讨论,最终我们决策网关统一打印所有的请求日志,理由如下:
减轻下游服务负担:请求日志记录在网关层可以减轻下游服务的负担。如果每个服务都负责记录请求日志,可能会导致日志冗余和增加服务的负担,而网关一般只处理请求的入口,可以更轻松地进行日志记录。
降低耦合性:通过在网关层记录请求日志,可以避免在每个微服务中都实现相同的日志记录逻辑。这降低了服务之间的耦合性,使得服务更专注于业务逻辑而不是日志记录。
更好的可扩展性:网关处的日志记录逻辑可以更容易地进行扩展。例如,可以根据需要添加额外的信息,进行自定义的日志处理,而不需要修改每个微服务的代码。
统一日志格式:在网关处记录请求日志可以确保日志格式的一致性。这对于日志的解析和分析工作非常有帮助,使得在整个系统中采用相同的日志格式,更易于处理和管理。
具体实现
在 Spring Cloud Gateway 中打印每个请求的请求参数和响应参数,可以通过自定义全局过滤器(Global Filter)来实现。以下是一个简单的示例,演示如何在全局过滤器中记录请求和响应的参数:
public class RequestResponseLoggingFilter implements GlobalFilter, Ordered {
private final Logger logger = LoggerFactory.getLogger(RequestResponseLoggingFilter.class);
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 打印请求参数
logRequestParams(request);
// 继续执行过滤器和路由
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 打印响应参数
logResponseParams(response);
}));
}
private void logRequestParams(ServerHttpRequest request) {
logger.info("Request Params:");
request.getQueryParams().forEach((key, values) ->
logger.info("{}: {}", key, values));
}
private void logResponseParams(ServerHttpResponse response) {
logger.info("Response Params:");
response.getHeaders().forEach((key, values) ->
logger.info("{}: {}", key, values));
}
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
业务背景
在 API 调用中增加验签功能的背景主要涉及到保障数据传输的完整性和安全性。以下是一些常见的背景和原因:
数据完整性保障:验签是一种保障数据完整性的手段。通过在请求中携带签名,服务端可以验证请求的参数在传输过程中是否被篡改。如果签名验证失败,说明请求数据可能被恶意篡改,服务端可以拒绝处理该请求,从而保障数据的完整性。
防止重放攻击:验签也可以用于防止重放攻击,即黑客通过记录有效请求,然后重复发送这些请求。通过使用时间戳等信息在签名中,可以使得同样的请求在不同时间产生不同的签名,从而防止黑客重复利用已知的请求。
安全传输敏感信息:在API调用中,有时候涉及敏感信息的传输,例如用户身份验证信息、支付数据等。通过使用签名,可以加密敏感信息,防止中间人攻击和信息泄露。
具体实现
在 Spring Cloud Gateway 中实现验签功能通常需要使用自定义的 Global Filter。以下是一个简单的示例,演示如何在 Spring Cloud Gateway 中实现基本的验签功能:
public class SignatureValidationFilter implements GlobalFilter, Ordered {
private static final String SECRET_KEY = "yourSecretKey"; // 替换为实际的密钥
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isValidSignature(exchange)) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
private boolean isValidSignature(ServerWebExchange exchange) {
// 获取请求参数
Map<String, String> params = exchange.getRequest().getQueryParams().toSingleValueMap();
// 验签逻辑,这里使用简单的MD5作为示例,实际情况需根据实际需求选择更安全的算法
String signature = generateSignature(params);
// 获取请求中的签名
String requestSignature = params.get("signature");
// 验证签名是否匹配
return signature.equals(requestSignature);
}
private String generateSignature(Map<String, String> params) {
// 对参数进行排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 构建待签名字符串
StringBuilder signContent = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
signContent.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
signContent.append(SECRET_KEY);
// 使用MD5进行签名,实际情况需要根据安全需求选择更强的算法
return md5(signContent.toString());
}
private String md5(String input) {
// md5算法 ...
}
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
上述示例是一个简单的验签过滤器,通过验证请求中的签名是否与计算得到的签名匹配来确定请求的合法性。这里使用了MD5作为示例,实际中应根据安全需求选择更强的算法。
请注意,以上示例中的密钥 SECRET_KEY 是一个示例密钥,实际情况中应该使用更长、更复杂的密钥,并且密钥的管理也是一个重要的安全考虑因素。此外,验签的具体实现和安全性要求需要根据实际需求进行调整。
图5 跨集群调用示意图
尽管本地缓存提升了性能,但在多节点环境下,需要仔细权衡性能提升与维护成本之间的关系。在未来的发展中,我们可能需要进一步考虑如何优雅地处理跨节点的缓存同步,以保证系统在性能和维护成本之间取得更好的平衡。
可以参考下面的代码来配置此参数:
4j
public class ReactorNettyConfig {
public ReactorResourceFactory reactorClientResourceFactory() {
// 设置select线程数
System.setProperty(ReactorNetty.IO_SELECT_COUNT, "1");
// 设置work线程数
int ioWorkerCount = Math.max(Runtime.getRuntime().availableProcessors()*4, 4);
System.setProperty(ReactorNetty.IO_WORKER_COUNT, String.valueOf(ioWorkerCount));
return new ReactorResourceFactory();
}
}
随着企业架构的不断演进和数字化转型的推进,网关承担着日益重要的角色。面对复杂多变的业务需求和技术生态,提升网关的卓越性能、稳定性以及灵活的配置与动态适应性成为刻不容缓的任务。在这一背景下,我们着眼于进一步加强以下方面,以确保网关能够紧密融入企业架构,为业务提供强大的流量管理和服务治理支持。
针对多样化的部署场景,优化网关的可伸缩性,使其能够轻松适应不同规模的业务。
在不断变化的业务环境中,网关的灵活性和动态适应性显得尤为重要。我们将强调配置管理的灵活性,支持动态调整网关行为,使其能够在不同的业务场景和需求下迅速适应。我们计划:
引入直观、易扩展的配置选项,使用户能够轻松调整网关的行为。
提供更细致的服务发现和负载均衡机制,确保流量能够精准而高效地传递到目标服务。
通过以上一系列的措施,我们致力于建立一个更加出色的网关系统,不仅在卓越的性能、稳定性、灵活性和可伸缩性方面取得显著进展,还能深度融入服务网格并提供全栈监控与分析的强大功能。这将有助于企业更好地应对不断变化的业务需求,提升系统的整体效能和可维护性。这也是我们未来在网关技术发展中持续努力的方向,以推动企业架构的更进一步的演进。
本文作者
海风,来自缦图互联网中心后端团队。
--------END--------
也许你还想看