2018年3月份选型时,刚好了解到springboot2的第二次发布,之前springboot2在国内发布,曾经出现Spring Boot 2.0 同步至 Maven 仓库出错,不过最终我还是选择了springboot2,我相信它应该不会再犯同样的错误。springboot1我用了,但是用的并不全,像erueka、zuul我们都没有用,当初只是想将我的服务轻量化,而我的服务治理2016年时就已经采用当当的dubbox,平台30多个系统,不能做太大的变化。现在选择springcloud做微服务,主要原因它主流,新系统可以试点,然后改造单点登录系统,让他兼容新旧框架就可以。
当我对zuul有点映像的时候,它已经过时了,SpringCloud Gateway就诞生,这让我们这些33岁的老程序员怎么活呢。看看纠错帖:Zuul & Spring Cloud Gateway & Linkerd性能对比,就明白一是因为spring专家也在探索,二呢zuul的设计模式可能影响到其他功能的扩展。不过也有不同的声音微服务API网关NGINX、ZUUL、Spring Cloud Gateway与。无论是gateway,还是zuul,难道希望做到性能跟nginx相比吗,应该是做不到的把。那么gateway到底是干啥的呢。nginx用作静态页面代理,而gateway或zuul用作微服务的负载均衡,毕竟他们是一个体系内的东西。
下图是spring cloud官网中显示gateway设计原理,看起来比zuul更为简洁。
zuul入门(1)zuul 的概念和原理,这篇文章别人整理出来的,看起来就复杂多了
我不想做性能测试,去质疑官方给出的结论,因为我做不了官方那么好的产品出来,而且官方不久也会推陈出新,所以干脆我就不管zuul了,闷头做gateway的调研得了。
下图是我这边的配置,你也可以参考Spring Cloud Gateway基于服务发现的默认路由规则中的说明。从下图可以看出这个api网关,管理那些微服务,那么就需要路由上面配置。官网上有多种配置策略,我的实际项目Route Predicate Factory中选择了Path,而GatewayFilter Factory选择的是StripPrefix。Path很好理解,就是根据路径去匹配,那么StripPrefix又是什么呢?其实很简单,就是跳过前缀的数量,下图描述比较清楚了。
When a request is made through the gateway to /name/bar/foo the request made to nameservice will look like http://nameservice/foo.
听了官网的注释,回过头想想,为什么会有这样的设计呢?聊聊spring cloud gateway的PrefixPath及StripPrefix功能,这样的设计莫非是为了省事,毕竟路径匹配也好花费时间,还是为了统一化呢?再看看这篇文章,SpringCloud Finchley基础教程:3,spring cloud gateway网关,如果id为空,那么就是uuid,那么也就是说api后台对应的系统可以是任意多台,甚至是重复的。相同的程序,但是对应的服务id不一样就可以,这样确实足够灵活,而且非常容易扩展,节点与节点之前还不会相互影响。
对下面语法不理解可以参考YAML最最基础语法
spring:
cloud:
gateway:
locator:
enabled: true
routes:
- id: gateway-auth
uri: lb://gateway-auth
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=2
- id: user-server
uri: lb://user-server
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- id: dcm-server
uri: lb://dcm-server
predicates:
- Path=/api/dcm/**
filters:
- StripPrefix=2
下面看一下GlobalFilter,下面介绍的过滤器,针对内部平台可能适用,比如有一些需要登录认证的,用户群体是单一的。如果内部用户,和外部用户都使用此网关,这个过滤器需要做调整。
package com.dzmsoft.gateway.api.filter;
import com.alibaba.fastjson.JSONObject;
import com.dzmsoft.gateway.api.feign.IUserService;
import com.dzmsoft.gateway.api.feign.ServiceAuthFeign;
import com.dzmsoft.gateway.auth.client.config.UserAuthConfig;
import com.dzmsoft.gateway.common.response.BaseResponse;
import com.dzmsoft.gateway.common.response.TokenForbiddenResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Configuration
@Slf4j
public class AccessGatewayFilter implements GlobalFilter {
@Autowired
@Lazy
private IUserService userService;
@Autowired
@Lazy
private ServiceAuthFeign serviceAuthFeign;
@Value("${gate.ignore.startWith}")
private String startWith;
@Autowired
private UserAuthConfig userAuthConfig;
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) {
log.info("check token and user permission....");
ServerHttpRequest request = serverWebExchange.getRequest();
final String requestUri = request.getPath().pathWithinApplication().value();
final String method = request.getMethod().toString();
ServerHttpRequest.Builder mutate = request.mutate();
// 不进行拦截的地址
if (isStartWith(requestUri)) {
ServerHttpRequest build = mutate.build();
return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());
}
String userId = null;
try {
userId = getJWTUser(request, mutate);
} catch (Exception e) {
log.error("用户Token过期异常", e);
return getVoidMono(serverWebExchange, new TokenForbiddenResponse("用户token错误或已过期!"));
}
ServerHttpRequest build = mutate.build();
return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());
}
/**
* 返回jwt中的用户信息
*
* @param request
* @param ctx
* @return
*/
private String getJWTUser(ServerHttpRequest request, ServerHttpRequest.Builder ctx) throws Exception {
List<String> strings = request.getHeaders().get(userAuthConfig.getTokenHeader());
String authToken = null;
if (strings != null) {
authToken = strings.get(0);
}
if (StringUtils.isBlank(authToken)) {
strings = request.getQueryParams().get("token");
if (strings != null) {
authToken = strings.get(0);
}
}
return serviceAuthFeign.getInfo(authToken);
}
/**
* 网关抛异常
*
* @param body
*/
// @NotNull
private Mono<Void> getVoidMono(ServerWebExchange serverWebExchange, BaseResponse body) {
serverWebExchange.getResponse().setStatusCode(HttpStatus.OK);
byte[] bytes = JSONObject.toJSONString(body).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(bytes);
return serverWebExchange.getResponse().writeWith(Flux.just(buffer));
}
/**
* URI是否以什么打头
*
* @param requestUri
* @return
*/
private boolean isStartWith(String requestUri) {
boolean flag = false;
for (String s : startWith.split(",")) {
if (requestUri.startsWith(s)) {
return true;
}
}
return flag;
}
/**
* 网关抛异常
*
* @param body
* @param code
*/
private Mono<Void> setFailedRequest(ServerWebExchange serverWebExchange, String body, int code) {
serverWebExchange.getResponse().setStatusCode(HttpStatus.OK);
return serverWebExchange.getResponse().setComplete();
}
}
1.[Spring Cloud Gateway全局过滤器GlobalFilter初探](https://segmentfault.com/a/1190000016252868),这篇文章可以了解一下GlobalFilter是干啥的,我这里主要是过滤token的获取的。
2.@Lazy
懒加载,等同于
,启动的时候并不装载,而在用的时候才会加载。那么什么时候不能加,什么时候能加呢?首先可以明确,一定是跟springboot启动有关的类,才有可能加@lazy。
3.mono是什么?参考[Mono入门应用](https://blog.csdn.net/songhaifengshuaige/article/details/79248343),这篇文章有些晦涩。
4 没有使用SpringCloudConfig的API网关配置
server:
port: 8765
servlet:
context-path: /api
spring:
application:
name: gateway-api
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null
cloud:
gateway:
locator:
enabled: true
routes:
- id: gateway-auth
uri: lb://gateway-auth
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=2
- id: user-server
uri: lb://user-server
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- id: dcm-server
uri: lb://dcm-server
predicates:
- Path=/api/dcm/**
filters:
- StripPrefix=2
- id: mp-server
uri: lb://mp-server
predicates:
- Path=/api/mp/**
filters:
- StripPrefix=2
gate:
ignore:
startWith: /jwt
auth:
serviceId: gateway-auth
user:
token-header: Authorization
eureka:
instance:
preferIpAddress: true
statusPageUrlPath: /actuator/info
healthCheckUrlPath: /actuator/health
client:
service-url:
defaultZone: http://192.168.5.102:8761/eureka
enabled: true
feign:
httpclient:
enabled: false
okhttp:
enabled: true
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
logging:
file: ${spring.application.name}.log