Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安
全,监控和限流。
优点:
1 性能强劲:是第一代网关Zuul的1.6倍
2 功能强大:内置了很多实用的功能,例如转发、监控、限流等
3 设计优雅,容易扩展
缺点:
1 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
2 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
3 需要Spring Boot 2.0及以上的版本,才支持
<!-- SpringCloud Alibaba Sentinel Gateway -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true # 让gateway可以发现nacos中的微服务
routes:# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务
# 认证中心
- id: com-xzx-auth # 当前路由的标识, 要求唯一
#lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
uri: lb://com-xzx-auth # 请求要转发到的地址
predicates: # 断言(就是路由转发要满足的条件)
- Path=/service-auth/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 当请求路径满足Path指定的规则时,才进行路由转发
- StripPrefix=1 # 转发之前去掉1层路径
# 商品中心
- id: com-xzx-shop
uri: lb://com-xzx-shop
predicates:
- Path=/service-shop/**
filters:
- StripPrefix=1
# 工单
- id: com-xzx-order
uri: lb://com-xzx-order
predicates:
- Path=/service-order/**
filters:
- StripPrefix=1
# 用户中心
- id: com-xzx-user
uri: lb://com-xzx-user
predicates:
- Path=/service-user/**
filters:
- LogGatewayFilterFactory=true,true
- StripPrefix=1
# 安全配置
security:
# 不校验白名单
ignore:
whites:
- /service-auth/auth/logout
- /service-auth/auth/login
- /service-auth/auth/login/test
- /service-auth/auth/register
Gateway 内置有许多的路由断言
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
-RemoteAddr=192.168.1.1/24
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
有给定名称且值与正则表达式匹配。
-Query=baz, ba.
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
//泛型 用于接收一个配置类,配置类用于接收中配置文件中的配置
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config
config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String ageStr =
serverWebExchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age = Integer.parseInt(ageStr);
return age > config.getMinAge() && age < config.getMaxAge();
}
return true;
}
};
}
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
class Config {
private int minAge;
private int maxAge;
}
# 自定义断言使用
routes:
- id: product-route
uri: lb://service-product
predicates:
- Path=/product-serv/**
- Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
filters:
- StripPrefix=1
gateway 过滤器分局部过滤器和全局过滤器,局部过滤要配置在具体的路由下,全局过滤器则对所有路由生效 全局过滤器可用来进行网关鉴权
/**
全局过滤器
* 网关鉴权
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
//网关放行白名单
private IgnoreWhiteProperties whiteProperties;
private RedisService redisService;
@Autowired
public AuthGlobalFilter(RedisService redisService,IgnoreWhiteProperties whiteProperties){
this.redisService = redisService;
this.whiteProperties = whiteProperties;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpRequest.Builder mutate = serverHttpRequest.mutate();
String url = serverHttpRequest.getURI().getPath();
//跳过不需要验证的路径
if (match(url,whiteProperties.getWhites())){
return chain.filter(exchange);
}
String token = getToken(serverHttpRequest);
if (StringUtils.isEmpty(token)){
return unauthorizedResponse(exchange,"令牌不能为空");
}
Claims claims = JWTUtils.parseToken(token);
if (Objects.isNull(claims)){
return unauthorizedResponse(exchange,"令牌过期或者不正确");
}
String userKey = JWTUtils.getUserKey(claims);
Boolean isLogin = redisService.hasKey(getTokenKey(userKey));
if (!isLogin){
return unauthorizedResponse(exchange,"登陆状态已过期");
}
String userId = JWTUtils.getUserId(claims);
String username = JWTUtils.getUsername(claims);
if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(username)){
return unauthorizedResponse(exchange,"令牌验证失败");
}
addHeader(mutate,"user_key",userKey);
addHeader(mutate,"user_id",userId);
addHeader(mutate,"username",username);
//内部请求来源参数清除
removerHeader(mutate,"from-source");
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void removerHeader(ServerHttpRequest.Builder mutate, String name) {
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private void addHeader(ServerHttpRequest.Builder mutate, String key, Object value) {
if (Objects.isNull(value)){
return;
}
String valueString = value.toString();
String valueEncode = ServletUtils.urlEncode(valueString);
mutate.header(key,valueEncode);
}
private String getTokenKey(String userKey) {
return "login_tokens:" + userKey;
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
log.error("[鉴权异常处理]请求路径{}",exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(),msg,401);
}
private String getToken(ServerHttpRequest serverHttpRequest) {
String token = serverHttpRequest.getHeaders().getFirst("Authorization");
if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer")){
token = token.replaceFirst("Bearer","");
}
return token;
}
@Override
public int getOrder() {
return 0;
}
private Boolean match(String url,List<String> whiteList){
if (StringUtils.isEmpty(url) || CollectionUtils.isEmpty(whiteList)){
return false;
}
for (String whiteUrl : whiteList) {
if (isMatch(url,whiteUrl)){
return true;
}
}
return false;
}
private Boolean isMatch(String url,String whiteUrl){
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(url,whiteUrl);
}
}
局部过滤器
@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
@Override
public String name() {
//重写name方法 这里就是配置中的名字,如果不重写 配置中就写Gateway前的 也就是log
return "LogGatewayFilterFactory";
}
public LogGatewayFilterFactory(){
super(LogGatewayFilterFactory.Config.class);
}
/**
* 读取配置文件中的参数 赋值到 配置类中
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
// 会把配置中的值赋值到 Config中
return Arrays.asList("consoleLog","cacheLog");
}
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
if (config.isCacheLog()){
System.out.println("缓存日志开启");
}
if (config.isConsoleLog()){
System.out.println("控制台日志开启");
}
return chain.filter(exchange);
};
}
//接收配置中的参数
public static class Config{
private boolean consoleLog;
private boolean cacheLog;
public boolean isConsoleLog() {
return consoleLog;
}
public void setConsoleLog(boolean consoleLog) {
this.consoleLog = consoleLog;
}
public boolean isCacheLog() {
return cacheLog;
}
public void setCacheLog(boolean cacheLog) {
this.cacheLog = cacheLog;
}
}
}
## 配置
filters:
- LogGatewayFilterFactory=true,true
- StripPrefix=1
/**
* 网关统一异常处理
*/
@Configuration
@Order(-1)
public class GatewayExceptionHandle implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandle.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted()){
//响应已经提交到客户端
return Mono.error(ex);
}
String msg;
if (ex instanceof NotFoundException){
msg = "服务未找到";
}else if (ex instanceof ResponseStatusException){
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
}else {
msg = "内部服务器错误";
}
log.error("[网关异常处理]请求路径:{},异常信息:{}",exchange.getRequest().getPath(),ex.getMessage());
return ServletUtils.webFluxResponseWriter(response,msg);
}
}
gateway依赖WebFlux所以要学习下WebFlux
WebFlux