1 简介
简单理解---业务服务的统一入口,方便实现,服务路由,安全,限流,过滤,黑白名单,证书加密解密,服务降级/熔断,灰度,等等
2 介绍
- Predicate(断言):如果请求路径与断言相匹配则进行路由。(路径匹配是常见断言)
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
- Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,路由成功,则可以执行pre Filter ,服务响应回数据,可以执行 Post Filter。
3 注意事项
3.1
spring-boot-starter-parent和spring-cloud-starter-gateway版本要一致,否则可能报错
4 基本路由实战
4.1 pom文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gateway</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2.1.3.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
</dependencies>
配置文件
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# config:
# server-addr: localhost:8848 # Nacos 服务器地址
gateway:
routes:
- id: goods-server #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://goods-server #匹配后提供服务的路由地址,lb负载
predicates:
- Path=/goods/** # 断言,路径相匹配的进行路由
- id: order-server
uri: lb://order-server
predicates:
- Path=/order/**
#- Path=/goods/,/goods/** 多个可以逗号隔开
enabled: true #开启网关,默认true 如果程序引用了spring-cloud-starter-gateway,但不希望启用网关 设置为false
4.2 测试路由转发
另外一个 goods 服务,直接访问 http://localhost:8080/goods/good/list 即可出现数据
直接访问gateway服务 http://localhost/goods/good/list 自己就会转发到goods服务上
4.3 StripPrefix
有一种场景,前端会在所有的接口前加一个api。前端请求的路径是 http://localhost/api/goods/good/list 。实际上请求 http://localhost/goods/good/list 怎么办
StripPrefix=1的意思就是去掉路径上第一个,也就是api将会去掉。 当我们访问。http://localhost/api/goods/good/list 它会自动变成 http://localhost/goods/good/list
5 限流
添加 pom坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
注意点:
配置文件中 redis-rate-limiter.replenishRate ,redis-rate-limiter.burstCapacity等同于配置bean中的 RedisRateLimiter,但是同时存在时,只有配置文件的配置生效。
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# config:
# server-addr: localhost:8848 # Nacos 服务器地址
gateway:
routes:
- id: goods-server #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://goods-server #匹配后提供服务的路由地址
predicates:
- Path=/goods/** # 断言,路径相匹配的进行路由
filters:
- name: RequestRateLimiter
args:
#spel表达式 它会去容器查找名称为hostKeyResolver 的Bean。
key-resolver: '#{@hostKeyResolver}'
#针对同一个key,允许的每秒请求数,不包括被抛弃的请求。这实际是令牌桶填充率。
redis-rate-limiter.replenishRate: 1
#针对同一个key,一秒内允许的最大请求数。这实际是令牌桶可容纳的最大令牌数。若设为0,则拒绝所有请求。
redis-rate-limiter.burstCapacity: 1
package com.order.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
* @创建人 赵伟
* @创建时间 2024/3/18
* @描述
*/
@Slf4j
@Configuration
public class RouteConfig {
/* @Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("userId")
//exchange.getRequest().getHeaders().getFirst("X-Forwarded-For") 基于请求ip的限流
);
}*/
/* *//**
* 根据 路径 进行限流
*
* @return KeyResolver
*//*
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getPath()).toString());
}*/
/**
* 根据 HostAddress 进行限流
*
* @return KeyResolver
*/
@Bean
public KeyResolver hostKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostString());
}
/**
* Redis 令牌桶 限流
* 这个等同于配置文件中的 redis-rate-limiter.replenishRate,redis-rate-limiter.burstCapacity
* 同时存在时,加载的是配置文件中的配置
* @return RedisRateLimiter
*/
@Bean RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(1, 1);
}
}
6黑白名单
/**
* @描述 禁用ip
*/
public class BlackIpFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String remoteIp = GatewayUtil.getRemoteIp(request);
//查询 ip接口-缓存等实现
if (true) {
//返回禁用提示码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}
7 权限或token/续时/传递用户信息/串行/
package com.order.filter;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.order.entity.MyUser;
import com.order.utils.GatewayUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @创建人 赵伟
* @创建时间 2024/3/20
* @描述 禁用ip
*/
@Component //需要将实现设置为Spring的组件
public class BlackIpFilter implements GlobalFilter, Ordered {
/**
* 顺序执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//1 ip黑名单
String remoteIp = GatewayUtil.getRemoteIp(request);
//查询 ip接口-缓存等实现
if (false) {
//返回禁用提示码
return loseToken(exchange,response);
// return exchange.getResponse().setComplete();
}
//2 OPTIONS 不允许
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
//返回禁用提示码
exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
return exchange.getResponse().setComplete();
}
//4 忽略的url
String pass = "/aaa/getToken|/aaa/updatePwd";
String uri = request.getURI().toString();
if (isPass(pass, uri)) {
return chain.filter(exchange);
}
//5 校验token
List<String> tokenList = request.getHeaders().get("token");
if (CollectionUtil.isEmpty(tokenList)) {
return loseToken(exchange,response);
}
//6 根据token 获取redis用户信息判断是否失效,
//注意多端系统生成token规则
String token = tokenList.get(0);
//自己写逻辑
MyUser user = queryCacheByToken(token);
if (ObjectUtil.isNull(user)) {
return loseToken(exchange,response);
}
//7 已经获取到用户了,判断用户状态,是够禁用/用户密码强制多少天修改。 登录接口也要实现,
boolean forbidden = false;//禁用
if (forbidden) {
return loseToken(exchange,response);
}
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
//put是覆盖
// queryParams.put("userId", Collections.singletonList("123"));
// add则不会覆盖,而是继续添加
// queryParams.add("userId","123");
//3 设置请求头加串行id
ServerHttpRequest.Builder mutate = request.mutate();
//不存在新增,存在即修改
mutate.header("serialId", UUID.randomUUID().toString());
//8 赋值用户信息 可用jwt加密 子服务 通过拦截器 在解析,放到ThreadLocal
//请求头中文乱码
// mutate.header("userInfo", String.valueOf(user));
String s = Base64.getEncoder().encodeToString(String.valueOf(user).getBytes());
mutate.header("userInfo", s );
ServerHttpRequest build = mutate.build();
exchange.mutate().request(build).build();
//9 token续时
//刷新token失效时间 是否每次请求都要续时,还是先获取判断小于一定时间在续时
// redisTemplate.setExpire(token, 60 * 60 * 24);
return chain.filter(exchange);
}
/**
* 假的获取缓存用户信息
* @param token
* @return
*/
private MyUser queryCacheByToken(String token) {
MyUser myUser = new MyUser();
myUser.setUserId("123");
myUser.setUserName("小明");
myUser.setUpdatePasswordTime(new Date());
return myUser;
}
/**
* 失效token
* @param exchange
* @param response1
* @return
*/
private Mono<Void> loseToken(ServerWebExchange exchange,ServerHttpResponse response1) {
// 设置响应的Content-Type头并指定编码为UTF-8
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
JSONObject responseBody = new JSONObject();
responseBody.put("result", Boolean.FALSE);
responseBody.put("msg", "您当前登录状态已失效,请重新登录");
responseBody.put("status", 400);
responseBody.put("isSuccess", Boolean.FALSE);
String s = JSON.toJSONString(responseBody);
byte[] bytes = new String(s.getBytes(), StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
/*
JSONObject jsonObject = new JSONObject();
jsonObject.put("code","401");
jsonObject.put("message","非法请求");
byte[] datas = JSON.toJSONString(jsonObject).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(datas);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));*/
}
/**
* 是否放行的url
* @param passString
* @param requestURI
* @return
*/
public static boolean isPass(String passString, String requestURI) {
if (StringUtils.isNotBlank(passString)) {
String[] split1 = passString.split("\\|");
for (int i = 0; i < split1.length; i++) {
String s = split1[i];
if (!StringUtils.isBlank(s) && StringUtils.containsIgnoreCase(requestURI, s)) {
return true;
}
}
}
return false;
}
}