09、SpringCloud之Gateway网关组件学习笔记(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 09、SpringCloud之Gateway网关组件学习笔记(二)

六、Filter过滤器工厂


6.1、介绍


介绍:gateway 里面的过滤器和 Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http 请求和返回Http响应。


分类:


按照生命周期:pre(在业务逻辑前)、post(在业务逻辑后)。


Filter在pre类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换、限流、token认证等。

Filter在post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等

按照种类区别:路由过滤器(某个路由单独使用)、全局过滤器(所有路由)。


路由过滤器(GatewayFilter):需要配置某个路由,才能过滤。如果需要使用全局路由,需要配置 Default。

全局过滤器(GlobalFilter):不需要配置路由,系统初始化作用到所有路由上 。

官网


官网-gatewayfilter-factories:包含31中单一路由过滤器。


官网-global-filters:包含9种全局路由过滤器。


6.2、自定义全局过滤器


自定义过程


我们基于之前的3.1案例的gateway-server来进行自定义。



自定义全局filter实现了一个GlobalFilter(过滤方法)、Ordered(执行顺序):


package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
/**
 * @Description: 自定义全局过滤器
 * @Author: changlu
 * @Date: 9:17 PM
 */
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求体以及请求对象
        ServerHttpRequest request = exchange.getRequest();
        // HttpServletRequest  这个是web里面的
        // ServerHttpRequest  webFlux里面 响应式里面的
        ServerHttpResponse response = exchange.getResponse();
        //通过请求对象可以拿到请求的一系列内容
        String path = request.getURI().getPath();//uri路径
        System.out.println("path:" + path);
        HttpHeaders headers = request.getHeaders();//请求头
        System.out.println("headers:" + headers);
        String name = request.getMethod().name();//请求方法名,也就是对应ip:port/xxx,这个/xxx
        System.out.println("method name:"+ name);
        String ip = request.getHeaders().getHost().getHostString();//获取到ip主机名
        System.out.println("ip:" + ip);
        //来进行测试响应数据
        // 用了微服务 肯定是前后端分离的 前后端分离 一般前后通过 json
        // {"code":200,"msg":"ok"}
        //1、设置响应头
        response.getHeaders().set("content-type", "application/json;charset=utf-8");
        //2、响应结果集封装
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.UNAUTHORIZED.value());
        result.put("msg", "暂未授权");
        ObjectMapper objectMapper = new ObjectMapper();//jackson工具类
        byte[] data = new byte[0];
        try {
            data = objectMapper.writeValueAsBytes(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //通过使用buffer工厂类来将其转为一个数据包(底层是基于netty,该对象底层是nio的bytebuffer)
        DataBuffer wrap = response.bufferFactory().wrap(data);
//        return response.writeWith(Mono.just(wrap));
        //放行过滤器
        return chain.filter(exchange);
    }
    //越小优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}


其中放行是执行chain的方法:


return chain.filter(exchange);


若是直接拦截结束,则是对response进行写数据:


//这个wrap包装成netty中的DataBuffer
return response.writeWith(Mono.just(wrap));


测试


直接gateway中进行拦截响应:




放行效果:



此时可以直接访问对应login-service接口:



该过滤器打印的一些request请求对象的信息:



6.3:实战4:实现一个ip拦截的过滤器


思路:同样也是在Gateway网关中添加一个全局过滤器组件。



package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @Description: IP检查过滤器
 * @Author: changlu
 * @Date: 8:41 AM
 */
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
    /**
     * 网关的并发比较高 不要再网关里面直接操作mysql
     * 后台系统可以查询数据库 用户量 并发量不大
     * 如果并发量大 可以查redis 或者 在内存中写好
     */
    private static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "192.168.1.1");
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取到请求对象化
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getHeaders().getHost().getHostString();
        //若是在集合中出现该ip,那么此时就拦截响应(一般黑名单可以存储在数据库中也可以存储的redis里)
        if (!BLACK_LIST.contains(ip)) {
            chain.filter(exchange);
        }
        //若是存在就进行拦截,并响应
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type", "application/json;charset=utf-8");
        Map<String, Object> result = new HashMap<>();
        result.put("code", 438);
        result.put("msg", "你已被拉黑,无法访问");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] data = new byte[0];
        try {
            data = objectMapper.writeValueAsBytes(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(data);
        return response.writeWith(Mono.just(wrap));
    }
    @Override
    public int getOrder() {
        return 1;
    }
}


测试一下:


可以看到localhost是在拦截范围内的,所以gateway会进行拦截响应:




6.4、实战5:在网关中实现token认证校验


在实战5中,我们完成的就是下图的第7步骤,也就是token进行认证校验是否合法来进行放行或直接响应!



说明:本章节的话会在login-service中完善doLogin接口,接着在gateway服务里添加一个认证token过滤器,并新建一个user-service并在其中添加一个接口对外使用。


注意:本章节的重点是在gateway中实现token认证来达到放行or错误响应,并不是在登录接口存储用户信息这些细节上,对于token生成、校验以及用户认证都仅仅只是做了简单的实现。


login-service(增加登录接口)
domain/user.java:
package com.changlu.loginservice.domain;
import lombok.Data;
import java.io.Serializable;
/**
 * @Description: 用户实体类
 * @Author: changlu
 * @Date: 9:20 AM
 */
@Data
public class User implements Serializable {
    private String username;
    private String password;
}



1、硬编码指定一个token。


private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";


2、添加一个doLogin接口,来用于获取token。


@PostMapping("/doLogin")
public String doLogin(@RequestBody User user) {
    System.out.println("dologin进行登录:" + user);
    //数据库进行认证,这里的话直接返回一个token
    return token;
}


user-service模块(新增,添加一个对外界接口)

说明:该模块主要是用于测试之后携带token的接口是否能够通过gateway认证并进行转发。



<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>



配置文件:application.yaml


server:
  port: 8082
spring:
  application:
    name: user-service
# 注册目标
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    hostname: localhost
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}


提供一个用户接口:仅仅是进行简单的用户返回。


package com.changlu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
 * @Description:
 * @Author: changlu
 * @Date: 9:16 AM
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping
    public Map<String, Object> getUser() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "成功获取到用户信息");
        return result;
    }
}



gateway-server模块(添加token认证过滤器)



1、添加一个token过滤器


package com.changlu.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
 * @Description: token检查过滤器
 * @Author: changlu
 * @Date: 9:25 AM
 */
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
    private static final String token = "700d7a8d-262a-447a-8254-9dd9ead6a0e2";
    private static final List<String> WHITE_PATH = Arrays.asList("/doLogin");
    /**
     * 流程:1、路径检测(是否放行)。2、请求头token获取。3、校验:放行or直接响应
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //放行一些公开接口
        String path = request.getURI().getPath();
        if (WHITE_PATH.contains(path)) {
            return chain.filter(exchange);
        }
        //从请求头中获取到Authorization
        List<String> authorization = request.getHeaders().get("Authorization");
        if (!ObjectUtils.isEmpty(authorization)) {
            String token = authorization.get(0);
            //去掉前缀"bearer "
            token = token.replaceFirst("Bearer ", "");
            //token校验,成功放行(实际上会进行token解析取到uuid来从redis中获取,这里简单来表示一下)
            if (TokenCheckFilter.token.equals(token)) {
                return chain.filter(exchange);
            }
        }
        //失败进行错误响应
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type", "application/json;charset=utf-8");
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.UNAUTHORIZED.value());
        result.put("msg", "暂未授权");
        ObjectMapper objectMapper = new ObjectMapper();//jackson工具类
        byte[] data = new byte[0];
        try {
            data = objectMapper.writeValueAsBytes(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(data);
        return response.writeWith(Mono.just(wrap));
    }
    @Override
    public int getOrder() {
        return 2;
    }
}


2、编写配置文件,新增一个路由



spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true   # 默认开启,只要加了网关依赖
      routes:
          # 用户服务路由
        - id: user-service-route
          uri: lb://user-service
          predicates:
            - Path=/user


测试



我们启动这四个模块,分别是:注册中心、网关、登录服务、用户服务。


来启动服务,以及查看一下eureka的注册中心服务注册情况:




接下来就可以开始进行测试了:我准备好两个接口



①测试doLogin接口是否能够放行并返回token



②测试用户服务接口


首先添加一下token,接着来发送请求




那我们来故意写错token来发送一下:




七、实战系列


7.1、实战6:实现请求限流


7.1.1、认识限流


通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:


IP 限流(5s 内同一个 ip 访问超过 3 次,则限制不让访问,过一段时间才可继续访问)

请求量限流(只要在一段时间内(窗口期),请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个 api(url),一个服务)


7.1.2、限流模型


介绍限流模型


限流模型:漏斗算法,令牌桶算法,窗口滑动算法,计数器算法。


常用的模型分类有两种:


时间模型

固定窗口模型:timeline 按照固定间隔分窗口,每个窗口有一个独立计数器,每个计数器统计窗口内的 qps,如果达到阈值则拒绝服务。最简单的限流模型,但是缺点比较明显,当在临界点出现大流量冲击,就无法满足流量控制。

滑动窗口模型:滑动时间模型会将每个窗口切分成 N 个子窗口,每个子窗口独立计数。这样用w1+w2计数之和来做限流阈值校验,就可以解决此问题。

桶模型

令牌桶:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

解决了在实际上的互联网应用中,流量经常是突发性的问题。

漏桶:水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

本章实战说明


本章节的话就使用Gateway内置的一个限流过滤器RequestRateLimiterGatewayFilterFactory:


也就是令牌桶限流模型:入不敷出


1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;


2)、根据限流大小,设置按照一定的速率往桶里添加令牌;


3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;


4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完


业务逻辑之后,将令牌直接删除;


5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令


牌,以此保证足够的限流;



7.1.3、Gateway 结合 redis 实现请求量限流(Gateway内置限流令牌桶实现)


集成过程


注意:Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,该过滤器是针对于某个路由的,并不是全局过滤器。



1、添加redis依赖


<!--   redis依赖    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>


2、指定限流的内容:ip或接口


config/RequestLimitConfig.java:
package com.changlu.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
/**
 * @Description: 请求限流配置类
 * @Author: changlu
 * @Date: 10:34 AM
 */
@Configuration
public class RequestLimitConfig {
    //针对某一个ip地址来进行限流(例如:localhost)
    @Bean(name = "ipKeyResolver")
    @Primary
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
    }
    //针对某一个接口uri来进行限流(例如:/doLogin)
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
}



3、配置文件为指定的路由配置filter:


配置文件:application.yaml



# redis参数配置
redis:
    host: localhost
    port: 6379
    database: 0
    password: 123456
# 配置路由
filters:
  - name: RequestRateLimiter
    args:
        key-resolver: '#{@ipKeyResolver}'
        redis-rate-limiter.replenishRate: 1 #令牌每秒填充速度
        redis-rate-limiter.burstCapacity: 1 #桶大小
        redis-rate-limiter.requestedTokens: 1 #默认是1,每次请求消耗的令牌数


测试



使用jmeter来进行测试:



若是请求失败,默认就会返回响应码为429。



看一下redis中存储的参数:



我们也可以换之前配置指定的另一个参数也就是接口名,此时redis中存储的如下:




7.2、实战7:Gateway集成跨域配置


对于ajax 同源策略,例如前端的访问端口与后端访问的端口不一致时,也就会产生跨域问题。


方式一:参数配置


spring:
  cloud:
    gateway:
        globalcors:
          cors-configurations:
            '[/**]':
              allowedOrigins: "*"
              allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION


方式二:通过java配置过滤器


@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}


测试


准备一个ajax的跨域问题:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="button" value="触发按钮" onclick="getData()">
<script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
    function getData() {
        //ajax请求
        $.get('http://localhost:81/doLogin',function(data){
            alert(data);
        });
    }
</script>
</body>
</html>





配置完跨域后再来进行测试:


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
负载均衡 Java 应用服务中间件
Gateway服务网关
Gateway服务网关
16 1
Gateway服务网关
|
1月前
|
XML Java 数据格式
如何使用 Spring Cloud 实现网关
如何使用 Spring Cloud 实现网关
28 3
|
2月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
92 5
|
3月前
|
安全 API
【Azure API 管理】APIM Self-Host Gateway 自建本地环境中的网关数量超过10个且它们的出口IP为同一个时出现的429错误
【Azure API 管理】APIM Self-Host Gateway 自建本地环境中的网关数量超过10个且它们的出口IP为同一个时出现的429错误
|
3月前
|
存储 容器
【Azure 事件中心】为应用程序网关(Application Gateway with WAF) 配置诊断日志,发送到事件中心
【Azure 事件中心】为应用程序网关(Application Gateway with WAF) 配置诊断日志,发送到事件中心
|
2月前
|
负载均衡 Java Nacos
SpringCloud基础2——Nacos配置、Feign、Gateway
nacos配置管理、Feign远程调用、Gateway服务网关
SpringCloud基础2——Nacos配置、Feign、Gateway
|
2月前
|
Java 开发者 Spring
Spring Cloud Gateway 中,过滤器的分类有哪些?
Spring Cloud Gateway 中,过滤器的分类有哪些?
44 3
|
1月前
|
负载均衡 Java API
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
37 0
|
2月前
|
安全 Java 开发者
强大!Spring Cloud Gateway新特性及高级开发技巧
在微服务架构日益盛行的今天,网关作为微服务架构中的关键组件,承担着路由、安全、监控、限流等多重职责。Spring Cloud Gateway作为新一代的微服务网关,凭借其基于Spring Framework 5、Project Reactor和Spring Boot 2.0的强大技术栈,正逐步成为业界的主流选择。本文将深入探讨Spring Cloud Gateway的新特性及高级开发技巧,助力开发者更好地掌握这一强大的网关工具。
208 6
|
4月前
|
负载均衡 Java Spring
Spring cloud gateway 如何在路由时进行负载均衡
Spring cloud gateway 如何在路由时进行负载均衡
475 15
下一篇
无影云桌面