传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,能够起到怎样的改善呢?
网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于:
(1)简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
(2)降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性
(3)解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑,
客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 确定请求与路由匹配,则将其发送到 Gateway Web Handler。此处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分开的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“预”过滤器逻辑。然后进行代理请求。发出代理请求后,运行“post”过滤器逻辑。
路由:网关的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则路由匹配。
谓词:这是一个Java 8 函数谓词。输入类型是Spring FrameworkServerWebExchange。这使您可以匹配 HTTP 请求中的任何内容,例如标头或参数。
Filter:这些是GatewayFilter使用特定工厂构建的实例。在这里,您可以在发送下游请求之前或之后修改请求和响应。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR9version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<version>2.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.9.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
dependency>
简单配置
server:
port: 8088
spring:
application:
name: gatewayService
cloud:
gateway:
#路由规则
routes:
#路由的唯一标识,自定义,一般设置为需要转发路由的服务名称
- id: order_route
#uri: http://localhost:8001 #硬编码匹配后提供服务的路由地址
#需要转发的地址
uri: lb://orderService #整合注册中心会匹配后提供服务的路由地址
#断言规则 用于路由规则的匹配
predicates:
#:路径路由谓词工厂
- Path=/orderService/**
#过滤器
filters:
#转发前去掉第一层路径
- StripPerfix=1
nacos:
#地址列表
server-addr: 127.0.0.1:8848
discovery:
username: nacos
password: nacos
namespace: public
sentinel:
transport:
dashboard: 127.0.0.1:8080 #sentinel控制台访问路径
port: 8080
eager: true #心跳启动
datasource:
ds:
nacos:
server-addr: 118.31.123.11:8848
dataId: gateway.json #对应nacos中添加的规则持久话配置文件名称
groupId: DEFAULT_GROUP
rule-type: flow
自定义断言工厂需要继承AbstractRoutePredicateFactory类重写apply方法的逻辑.在apply方法可以通过exchange.getRequest()拿到ServerHttpRequest对象\请求方式\请求头等信息.
== 注意 : 命名需要以 RoutePredicateFactory 结尾 ==
/**
* 名称必须是xxxRoutePredicateFactory形式
* todo:模拟授权的验证,具体逻辑根据业务完善
* 第一步:自定义一个断言类名字以RoutePredicateFactory结尾,并且继承AbstractRoutePredicateFactory抽象类
* 第四步:在yaml中配置自定义断言规则,断言名称就是自定义类的前缀HeadersAuth
*/
@Component //需要添加到到spring容器中去
public class HeadersAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<HeadersAuthRoutePredicateFactory.Config> {
public HeadersAuthRoutePredicateFactory() {
super(HeadersAuthRoutePredicateFactory.Config.class);
}
//从配置文件中读取参数并赋值到config配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("headers", "strategy");
}
//第三步: 重写apply方法,在test方法中编写断言匹配规则逻辑代码
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
List<String> headers = config.getHeaders();
if (headers == null || headers.isEmpty()) {
return true;
}
switch (config.getStrategy()) {
case AND:
//策略是AND 时,请求头中必须包含所有header
return headers.stream().allMatch(header -> existHeaderFunc.apply(httpHeaders,header));
default:
//策略为OR时, 请求头包含任意一header即可
return headers.stream().anyMatch(header -> existHeaderFunc.apply(httpHeaders,header));
}
}
@Override
public String toString() {
return String.format("Config: %s", config.toString());
}
};
}
BiFunction<HttpHeaders, String, Boolean> existHeaderFunc = (httpHeaders, header) -> Optional.ofNullable(httpHeaders.get(header)).filter(list -> !list.isEmpty()).isPresent();
public enum Strategy {
AND, OR;
}
//第二步: 定义配置类,这里对应的是在yaml中需要匹配的断言规则
public static class Config {
//配置的header, 可以配置多个
private List<String> headers = new ArrayList<>();
//当配置多个时,校验策略, and 还是 or
private Strategy strategy = Strategy.OR;
public List<String> getHeaders() {
return headers;
}
public Strategy getStrategy() {
return strategy;
}
public Config setHeaders(List<String> ignorePatterns) {
this.headers = ignorePatterns;
return this;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
@Override
public String toString() {
return new ToStringCreator(this).append("headers", headers).append("strategy", strategy).toString();
}
}
}
作用: 在请求传递过程中对请求和响应根据业务需求做处理
生命周期: PRE (请求处理之前) / POST(请求处理之后)
分类: 局部过滤器(作用在某一个路由上) GatewayFilter工厂 / 全局过滤器(作用在全部路由上) GlobalFilter
自定义;局部过滤器
/**
* 名称必须是xxxGatewayFilterFactory形式
* todo:模拟授权的验证,具体逻辑根据业务完善
* 第一步:自定义局部过滤器类名字以GatewayFilterFactory结尾
* 第四步:在yaml中需要处理的路由上添加自定义拦截器,并定义规则,拦截器关键字为自定义拦截器类的前缀,例如: Authorize
*/
@Component
@Slf4j
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
private static final String AUTHORIZE_TOKEN = "token";
//构造函数,加载Config
public AuthorizeGatewayFilterFactory() {
//固定写法
super(AuthorizeGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
//Config.enabled
return Arrays.asList("enabled");
}
//第三步:重写apply方法,根据业务需要编写业务处理逻辑
@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
//判断是否开启授权验证
if (!config.isEnabled()) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
//从请求头中获取token
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
//从请求头参数中获取token
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
ServerHttpResponse response = exchange.getResponse();
//如果token为空,直接返回401,未授权
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//处理完成,直接拦截,不再进行下去
return response.setComplete();
}
/**
* todo chain.filter(exchange) 之前的都是过滤器的前置处理
*
* chain.filter().then(
* 过滤器的后置处理...........
* )
*/
//授权正常,继续下一个过滤器链的调用
return chain.filter(exchange);
};
}
//第二部定义配置类
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
// 控制是否开启认证
private boolean enabled;
}
}
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
开发中的鉴权逻辑:
。当客户端第一次请求服务时,服务端对用户进行信息认证 (登录)
。认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
。以后每次请求,客户端都携带认证的token
。服务端对token进行解容,判断是否有效
实现implements GlobalFilter, Ordered两个接口,然后重写两个方法即可。一个是filter方法,一个是getOrder方法。全局过滤器可以存在多个,多个的时候根据getOrder方法的返回值大小就行排序执行,数字最小的过滤器优先执行。并且全局过滤器无需配置 叫给Spring管理后就会生效
@Component //必须加,必须加,必须加
public class MyLogGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("time:" + new Date() + "\t 执行了自定义的全局过滤器: " + "MyLogGlobalFilter" + "hello");
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
System.out.println("****用户名为null,无法登录");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
// 这个就是继续执行的意思
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
第一张方式: 配置文件配置
spring:
cloud:
sentinel:
#配置限流之后的响应内容
scg:
fallback:
# 两种模式:一种是response返回文字提示信息,一种是redirect,重定向跳转,需要同时配置redirect(跳转的uri)
mode: response
# 响应的状态
response-status: 426
# 响应体
response-body: '{"code": 426,"message": "限流了,稍后重试!"}'
第二种方式: 配置类配置
sentinel启动配置类添加,和限流后异常处理
@Slf4j
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置 限流后异常处理 JsonSentinelGatewayBlockExceptionHandler重写 SentinelGatewayBlockExceptionHandler
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 配置SentinelGatewayFilter
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
@Slf4j
@Component
public class JsonSentinelGatewayBlockExceptionHandler implements WebExceptionHandler{
@Autowired
private ObjectMapper objectMapper;
private List<ViewResolver> viewResolvers;
private List<HttpMessageWriter<?>> messageWriters;
private final Supplier<ServerResponse.Context> contextSupplier = () -> {
return new ServerResponse.Context() {
public List<HttpMessageWriter<?>> messageWriters() {
return JsonSentinelGatewayBlockExceptionHandler.this.messageWriters;
}
public List<ViewResolver> viewResolvers() {
return JsonSentinelGatewayBlockExceptionHandler.this.viewResolvers;
}
};
};
public JsonSentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolvers;
this.messageWriters = serverCodecConfigurer.getWriters();
}
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
} else {
return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> {
return this.writeResponse(response, exchange);
});
}
}
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
/** 只需要修改此方法 */
public Mono<Void> writeResponse(ServerResponse serverWebExchange, ServerWebExchange exchange) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
return unAuth(serverHttpResponse, "访问的人太多了,请稍后再试!");
}
private Mono<Void> unAuth(ServerHttpResponse resp, String msg) {
resp.setStatusCode(HttpStatus.FORBIDDEN);
resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String result = "";
try {
result = objectMapper.writeValueAsString(ResponseProvider.unAuth(msg));
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
}
可以在网关利用springboot提供的CorsWebFilter,对请求头添加跨域的信息
@Configuration
public class CorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//配置跨域
CorsConfiguration config = new CorsConfiguration();
//任意请求头
config.addAllowedHeader("*");
//任意方式
config.addAllowedMethod("*");
//任意请求来源
config.addAllowedOrigin("*");
//允许携带cookie跨域
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
也可以在网关直接配置解决跨域问题:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://docs.spring.io"
allowedMethods:
- GET
- POST
- DELETE