API网关简介
在使用微服务架构之后,系统中会有很多微服务,有些微服务需要用户满足特定的权限才可以访问,比如查看购买历史记录,查看优惠信息等等,这需要用户登录之后才能看到,倘若在每个微服务中都编写处理用户登录代码的话,势必会出现冗余,我们可以将这些代码放到API网关中,统一处理。其实API网关就是指定了程序的统一入口,我们可以将一些业务无关的公共部分代码放到这里,比如认证,鉴权,路由,监控等。
市面上比较知名的网关技术:
- Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,spring cloud alibaba目前没有提供网关,我们可以使用这个。 - Ngnix
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用。 - Kong
性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。 - ZuulNetflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
Spring Cloud Gateway简介
早期spring cloud版本中集成了zuul作为网关,但由于zuul 1.x的性能问题和zuul 2.x不断延期,于是spring自己开发了网关spring cloud gateway,该技术基于spring5,spring boot2.0版本开发,性能方面比zuul1.x好一些。gateway底层使用了netty,所以需要netty环境来运行,不能使用我们所熟悉的tomcat等servlet容器来运行。
Spring Cloud Gateway的路由
路由(route)的作用是将请求的url分配到相应的程序上,这里我们使用路由就是将请求分配到相应的微服务中。在spring cloud gateway的路由route中包含下面内容:
- id:路由的id,保证唯一性。
- uri:路由将请求分配的 uri,即客户端请求最终被转发的微服务。
- order:用于多个路由之间的排序,数值越小优先级越高。
- predicate:如果请求地址匹配当前规则的话,则会转发到uri指定的微服务上。
- filter:过滤器用于修改请求和响应信息。
接下来使用spring cloud gateway实现一个简单的路由功能。
1.创建新的模块并添加相关依赖,注意不要添加web依赖(里面包含了tomcat)。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.application启动类上添加@EnableDiscoveryClient开启服务发现
@SpringBootApplication
@EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.application.yml中添加网关相关配置
server:
port: 80
spring:
cloud:
application:
name: gateway-service
gateway:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos注册中心地址
discovery:
locator:
enabled: true # 开启后,gateway可以发现nacos中的服务
routes: # 路由数组,指定当请求跳转到相应的微服务
- id: buy-route # 当前路由的id,保证唯一性
uri: lb://buy-service # lb指的是负载均衡,buy-service是nacos中的微服务名字
order: 1 #路由的优先级,数字越小级别越高
predicates:
- Path=/buy-path/** # 当url匹配/buy-path时,进行路由转发到上面uri配置的微服务中
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
上面倘若不配置filters的话,当我们发出下面请求时http://localhost/buy-path/buy
,gateway会根据配置访问http://localhost:8086/buy-path/buy
,这里多了一级目录buy-path
,配置上面的filters之后,gateway会取消掉这级目录。
4.启动各个相关微服务,浏览器中访问http://localhost/buy-path/buy
即可看到效果。
gateway工作方式
客户端gateway client向Spring Cloud Gateway发出请求,handler mapping会对url与设定的谓词进行匹配,失败会返回404,成功则将请求发送到Web handler进行处理,然后会调用一系列的filter过滤器,最终请求到达proxied service(即微服务)。
路由断言工厂
在spring cloud gateway中内置了很多路由断言工厂,这些断言工厂跟HTTP请求参数做匹配,我们使用这些断言工厂可以非常方便的对当前请求进行限制。
- datetime断言工厂根据日期进行判断,分为after(表示判断请求日期是否晚于指定日期),before(判断请求日期是否早于指定日期),between(接收两个日期参数,判断请求日期是否在指定时间段内)。分别对应的类是
AfterRoutePredicateFactory
,BeforeRoutePredicateFactory
,BetweenRoutePredicateFactory
下面是after的设置,当请求的时间晚于设置的时间才可以访问到uri中的服务。在项目上线新的功能时可以使用after设置。spring: cloud: gateway: routes: - id: after_route uri: http://www.monkey1024.com predicates: - After=2022-02-01T17:42:47.789+08:00[Asia/Shanghai]
- cookie断言工厂对应的类是CookieRoutePredicateFactory,接收两个参数,判断请求中是否包含某个cookie且值是否匹配正则表达式。下面配置表示请求中的的cookie名字必须要有username并且其值需要是monkey1024
spring: cloud: gateway: routes: - id: cookie_route uri: http://www.monkey1024.com predicates: - Cookie=username, monkey1024
- header断言工厂对应的类是
HeaderRoutePredicateFactory
,接收两个参数,判断请求中是否包含某个cookie且值是否匹配正则表达式。下面配置表示请求头中包含X-Request-Id并且值是一个正整数。spring: cloud: gateway: routes: - id: header_route uri: https://monkey1024.com predicates: - Header=X-Request-Id, \d+
- host断言工厂对应的类是
HostRoutePredicateFactory
,判断请求的host是否满足条件。spring: cloud: gateway: routes: - id: host_route uri: https://monkey1024.com predicates: - Host=**.taobao.com,**.qq.com
- method断言工厂对应的类是
MethodRoutePredicateFactory
,判断请求的方式是否满足条件。下面表示请求方式必须是get或者post。spring: cloud: gateway: routes: - id: method_route uri: https://monkey1024.com predicates: - Method=GET,POST
- path断言工厂对应的类是
PathRoutePredicateFactory
,判断请求的路径是否满足条件。spring: cloud: gateway: routes: - id: path_route uri: https://monkey1024 predicates: - Path=/user/{segment},/product/{segment}
上面表示请求路径是user/jack或者product/car,才会匹配,其中jack和car可以是其他值。通过下面程序可以获取到:
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); String segment = uriVariables.get("segment");
- query断言工厂对用的类是
QueryRoutePredicateFactory
,接收两个参数,判断请求的参数名字和值是否匹配。下面表示请求的参数需要有red,并且值要匹配gree.的正则,比如green,greet都满足。spring: cloud: gateway: routes: - id: query_route uri: https://monkey1024.com predicates: - Query=red, gree.
- remoteAddr断言工厂对应的类是
RemoteAddrRoutePredicateFactory
,判断请求主机的ip地址是否满足指定的ip段。比如当请求主机的IP是192.168.1.10的时候是满足的。spring: cloud: gateway: routes: - id: remoteaddr_route uri: https://monkey1024.com predicates: - RemoteAddr=192.168.1.1/24
- weight断言工厂对应的类是
WeightRoutePredicateFactory
,接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发。下面设置表示倘若有10个请求,8个请求会转发到weighthigh上面,2个请求会转发到weightlow上面spring: cloud: gateway: routes: - id: weight_high uri: https://weighthigh.org predicates: - Weight=group1, 8 - id: weight_low uri: https://weightlow.org predicates: - Weight=group1, 2
自定义断言工厂
当spring gateway内置的自定义工厂不能满足我们的需求时,可以自定义断言工厂,这里定义一个只允许id值在[1,100]范围内才能访问的断言工厂。
1.修改配置文件,在predicates下添加Id=1,100
,注意首字母大写
server:
port: 8000
spring:
cloud:
application:
name: gateway-service
gateway:
nacos:
discovery:
server-addr: 127.0.0.1:8848
discovery:
locator:
enabled: true
routes:
- id: buy-route
uri: lb://buy-service
order: 1
predicates:
- Path=/buy-path/**
- Id=1,100 #限制id在1~100之间的可以访问
filters:
- StripPrefix=1
2.创建AbstractRoutePredicateFactory
的子类,里面定义一个静态内部类Config
,该类的作用是读取配置文件中的数据。重写的apply方法中的return里面编写断言规则,如果返回true则表示可以通过,false表示不允许通过。
package com.monkey1024.predicate;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
@Component
public class IdRoutePredicateFactory extends AbstractRoutePredicateFactory<IdRoutePredicateFactory.Config> {
public IdRoutePredicateFactory() {
super(IdRoutePredicateFactory.Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(IdRoutePredicateFactory.Config consumer) {
return serverWebExchange -> {
//获取当前请求的参数id
String id = serverWebExchange.getRequest().getQueryParams().getFirst("id");
//这里返回true表示匹配,返回false表示不匹配
//判断参数id是否满足条件
if (Objects.nonNull(id)) {
int i = Integer.parseInt(id);
return i > consumer.min && i < consumer.max;
}
return true;
};
}
@Override
public List<String> shortcutFieldOrder() {
//方法参数中的顺序要于配置文件中的一致
return Arrays.asList("min", "max");
}
//结合上面的shortcutFieldOrder方法,会将配置文件的数据读取进来
@Data
public static class Config {
private int min;
private int max;
}
}
过滤器
spring cloud gateway提供了过滤器,这个跟servlet中的过滤器或者spring mvc中的拦截器比较像,gateway中使用pre对请求进行处理,post对响应进行处理。这里过滤器分为了如下两种:
- 局部过滤器(GatewayFilter),局部过滤器可以对某个路由进行过滤。
- 全局过滤器(GlobalFilter),全局过滤器可以对全部路由进行过滤。
局部过滤器
在gateway官网中可以看到内置了很多局部过滤器,使用方式跟断言类似,这里我们以RedirectTo
为例,将请求重定向。相关配置
server:
port: 8000
spring:
cloud:
application:
name: gateway-service
gateway:
nacos:
discovery:
server-addr: 127.0.0.1:8848
discovery:
locator:
enabled: true
routes:
- id: buy-route
uri: lb://buy-service
order: 1
predicates:
- Path=/buy-path/**
filters:
- StripPrefix=1
- RedirectTo=302,http://www.monkey1024.com #设置状态码和重定向的地址
自定义局部过滤器
当内置的局部过滤器不能满足我们的需求时,可以自定义局部过滤器。在配置文件中filters下添加配置
server:
port: 8000
spring:
cloud:
application:
name: gateway-service
gateway:
nacos:
discovery:
server-addr: 127.0.0.1:8848
discovery:
locator:
enabled: true
routes:
- id: buy-route
uri: lb://buy-service
order: 1
predicates:
- Path=/buy-path/**
filters:
- StripPrefix=1
- Flag=true
编写自定义局部过滤器工厂,方式跟之前的自定义断言工厂类似
package com.monkey1024.predicate;
import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/*
定义局部过滤器
*/
@Component
public class FlagGatewayFilterFactory extends AbstractGatewayFilterFactory<FlagGatewayFilterFactory.Config> {
public static final String FLAG_KEY = "flag";
public FlagGatewayFilterFactory() {
super(FlagGatewayFilterFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(FLAG_KEY);
}
//过滤器中的内容
@Override
public GatewayFilter apply(FlagGatewayFilterFactory.Config consumer) {
return (exchange, chain) -> {
if (consumer.flag) {
System.out.println("flag是true");
} else {
System.out.println("flag是false");
}
return chain.filter(exchange);
};
}
//获取配置文件中的数据
@Data
public static class Config {
private boolean flag;
}
}
全局过滤器
spring cloud gateway中还提供了一些全局过滤器,具体可以查看其官方文档。我们之前在配置文件中uri部分写的lb就使用了全局过滤器LoadBalancerClientFilter
。下面我们来编写一个自定义的全局过滤器,该过滤器的作用是判断请求中是否携带token。
package com.monkey1024.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
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;
import java.util.Objects;
/*
全局过滤器
*/
@Component
public class TokenGlobeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (Objects.isNull(token)) {
System.out.println("没有token,不予放行");
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
//过滤器的优先级,数字越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
网关限流
使用网关之后,它就成为了整个系统的入口,因此我们可以在网关层面进行限流。这里使用之前学过的sentinel集成gateway进行限流。sentinel提供了两种维度的限流:
- route维度:针对每个配置文件中的route进行限流
- API维度:针对一组route进行限流,可以很好的复用限流规则
下面的操作是将限流规则放入到内存中,若要持久化,方式与sentinel中的持久化操作一致。
route维度
添加依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
配置文件添加sentinel dashboard
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080
创建网关配置类
package com.monkey1024.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@Configuration
public class GatewayRouteConfiguration {
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
打开sentinel控制台,里面配置限流规则即可。
可以在上面的类中添加方法来设置限流后返回的内容。
@Configuration
public class GatewayRouteConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayRouteConfiguration(ObjectProvider<List<ViewResolver>>
viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers =
viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "你的刷新过快,请稍后再试");
return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).
body(BodyInserters.fromValue(map));
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
我们还可以在上面的类中设置初始的限流,在网关启动后这些设置就会生效,添加下面方法设置
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("buy-route") //配置文件中的路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
API维度
api维度需要在代码中定义路由谓词集合和分组,将集合放入对应的分组中,然后再对分组设置限流规则,这样就可以将一个限流规则使用到多个路由上。
创建类编写下面内容,然后再启动路由即可看到限流效果:
@Configuration
public class GatewayApiConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayApiConfiguration(ObjectProvider<List<ViewResolver>>
viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers =
viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//加入api组
rules.add(new
GatewayFlowRule("buy-path").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
//自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
//定义谓词set,将要限流的路径传入
Set<ApiPredicateItem> buyPathItem = new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/buy-path/buy/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}};
//定义一个分组名为buy-path,将上面的路由谓词set传入
ApiDefinition api = new ApiDefinition("buy-path")
.setPredicateItems(buyPathItem);
definitions.add(api);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "你的刷新过快,请稍后再试");
return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).
body(BodyInserters.fromValue(map));
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
跨域问题
如果 访问的host,协议,端口都相同,则称之为同源(不跨域),否则为非同源(跨域)。比如有一个页面地址为:
http://www.monkey1024.com/content/way.html
该页面中的js以ajax或axios等方式访问下面地址是否跨域:
url | 是否跨域 |
---|---|
https://www.monkey1024.com/content/user | 是,协议不同,http和https |
http://www.monkey1024.com:8080/content/user | 是,端口号不同 |
http://www.monkey.com/content/user | 是,host不同 |
http://www.monkey1024.com/content/user | 否 |
之前经常会在自己写的html中引入其他域名下的js,css,图片等,例如:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<div id="app">
<!--引入外部图片-->
<img src="http://monkey1024.com/images/logo.png"/>
</div>
<!--引入外部js-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</body>
</html>
这种写法并未出现跨域问题,原因是浏览器是支持跨域的,但是在JavaScript代码中是不能进行跨域请求,示例如下,vue通过axios发送一个跨域请求,axios的请求地址是之前微服务的网关,双击html打开(确保跟之前的网关满足跨域条件),点击购买按钮后,浏览器f12可以看到跨域请求错误:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<div id="app">
<button @click="buy">购买</button>
<div v-text="message"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: ''
},
methods: {
buy: function () {
let that = this
axios.get("http://localhost/buy-path/buy").then(function (response) {
console.log(response)
that.message = response.data
}, function (error) {
})
}
}
});
</script>
</body>
</html>
我们可以在网关中编写下面代码来解决跨域问题
package com.monkey1024.config;
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;
import org.springframework.web.util.pattern.PathPatternParser;
/*
解决跨域问题
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//*表示所有
config.addAllowedMethod("*");//允许的请求方法,get、post、put、delete等
config.addAllowedOrigin("*");//允许哪个域名的请求
config.addAllowedHeader("*");//允许的请求头
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource(new PathPatternParser());
/*
允许哪个请求进行跨域,如果只允许/buy-path下的请求跨域,则
source.registerCorsConfiguration("/buy-path/*", config);
*/
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}