spring cloud gateway (五)

SpringCloudGateway是基于Spring5和SpringBoot2构建的高性能网关,用于替代Zuul,提供API路由管理。它支持动态路由、过滤器、安全、限流等功能,并能与Sentinel、Nacos等集成。文章详细介绍了Gateway的配置,如路由断言(如Path、Cookie、Header等)和过滤器(全局与局部),以及自定义断言和异常处理策略。
摘要由CSDN通过智能技术生成

Gateway简介

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及以上的版本,才支持
在这里插入图片描述

Gateway

 		<!-- 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 内置有许多的路由断言

基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
基于Query请求参数的断言工厂
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

在这里插入图片描述

gateway自定义断言

//泛型 用于接收一个配置类,配置类用于接收中配置文件中的配置
    @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 过滤器

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

gateway 统一异常处理

/**
 * 网关统一异常处理
 */
@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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会跑的葫芦怪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值