六、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>
配置完跨域后再来进行测试: