网关简介:
网关是微服务最边缘的服务(网关也是一个服务),直接暴露给用户,用来做用户和微服务的桥梁。
网关作为微服务架构系统的统一入口,为微服务架构系统提供简单、有效且统一的API路由管理(提供内部服务的路由中转),还可以实现一些和业务没有耦合的公用逻辑,如token拦截、权限验证,限流、监控日志等操作。
网关和服务调用的区别:
使用网关的时候:浏览器发来的请求会先到达网关后,由网关对请求进行分发,分发到注册中心中对应的服务集群,并进行负载均衡处理
不使用网关的时候,浏览器需要指定到达的服务集群中的某个服务的ip和端口号,才能实现调用;
总结:使用网关简化了浏览器发来的请求的分发,将网关直接暴露给客户,做客户和微服务的桥梁
网关不是服务调用,服务调用是指消费者服务调用提供者服务,那是ribbon和openFeign干的事;所以网关并不是消费者服务,因为服务调用中消费者服务和提供者服务都是有做请求处理的(有controller)
网关只是做服务的路由中转,将请求中转给服务,其不做请求处理。— 服务调用和服务路由的前提都是先做服务发现。
Nginx和Gateway的区别
网关是介于nignx以及应用服务之间的中间层,主要负责将请求路由到不同的微服务中以及对请求的合法性进行校验。
Gateway是前端服务到后台服务之间的一个对内网关(路由),nginx是用户到前端服务的一个对外网关(路由)。
工作流程:
客户端向SpringCloud Gateway发出请求,SpringCloud Gateway通过Gateway Handler Mapping找到与请求相匹配的路由,然后将请求发送到Gateway Web Handler,Gateway Web Handler经过指定的过滤器,然后将请求发送到实际的服务的业务逻辑,最后再返着回来。
总结:Gateway 的核心逻辑就是 路由转发+执行过滤器链
核心概念:
Route(路由)
一个路由由一个id、一个目的url、一组断言、一组过滤器组成;如果路由断言为真,说明请求url和配置的路由匹配。Predicate(断言)
断言就是一些布尔表达式,满足条件的返回true,不满足条件的返回false。
SpringCloud Gateway的断言允许开发者去定义判断来自Http请求中的所有信息,比如请求路径、请求参数等,以此对路由进行匹配(判断Http请求中请求路径、请求参数等是否满足一定的条件,以此来匹配相应的路由)。
Filter(过滤器)
gateway里面的过滤器和Servlet里面的过滤器功能差不多,都有拦截请求,在请求前后进行处理的作用。
使用:
第一步:创建项目
<!--因为网关也属于服务,所以网关的项目需要导入EurekaClient的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入的gateway的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
第二步:主启动类
//网关服务也需要注入到注册中心去,所以网关项目的启动类上也需要添加注解 @EnableEurekaClient
第三步:application.properties文件/java代码===>配置Gateway
#设置Gateway网关服务名称 -- 为gateway-80 spring.application.name=gateway-80 #设置Gateway网关端口 -- 为80 server.port=80 #将Gateway网关服务注册到注册中心 -- 值为eureka服务端(注册中心)url地址 eureka.client.service-url.defaultZone=http://localhost:8761/eureka #---------------------------Gateway指定路由的配置-------------------------- #开启网关(默认就是开启的) spring.cloud.gateway.enabled=true #配置路由(spring.cloud.gateway.routes属性值是List): #设置路由id(任意,唯一即可) spring.cloud.gateway.routes[0].id=user-service-router #设置路由到的服务的uri(ip:port) #使用http协议配置的时候,适用于单个服务来进行配置,不适用于集群配置 spring.cloud.gateway.routes[0].uri=http://localhost:8081 #使用lb协议配置的时候,适用于集群配置,同时对集群进行负载均衡调用,采用轮询算法; #负载均衡算法的修改参考Ribbon修改负载均衡算法的方式 spring.cloud.gateway.routes[0].uri=lb://服务名 #设置路由断言(路由到的服务中的请求路径的匹配规则) spring.cloud.gateway.routes[0].predicates[0]=Path=/路径 #---------------------------Gateway动态路由的配置-------------------------- #开启动态路由 spring.cloud.gateway.discovery.locator.enabled=true #允许服务名小写 spring.cloud.gateway.discovery.locator.lower-case-service-id=true
//创建配置类 //也是指定服务的写法 //添加配置类注解 //使用java代码配置,可以在代码中进行一些逻辑判断,比如安全验证等等 @Configuration public class GatewayConfig { //配置RouteLocator的Bean对象到IOC容器 @Bean public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder){ //创建RouteLocatorBuilder.Builder对象 RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); //调用RouteLocatorBuilder.Builder对象的route()方法配置路由 //联掉方式 routes .route("user-service-router",//路由id r -> r.path("/info/**")//路由断言(路由到的服务中的请求路径的匹配规则) .uri("http://localhost:8081")//路由到的服务的uri(ip:port) ) .build(); //调用RouteLocatorBuilder.Builder对象的build()方法创建RouteLocator对象并返回 return routes.build(); } }
补充:
#单一配置时候浏览器的访问路径: localhost:端口/路径 #集群配置时候的浏览器的访问路径 localhost/路径 或 localhost/服务名/路径
#application.properties中对于gateway的单一的配置和集群配置可搭配一起使用
断言的概述
断言就是一些布尔表达式,满足条件的返回true,不满足条件的返回false。
SpringCloud Gateway的断言允许开发者去定义判断来自Http请求中的所有信息,以此对路由进行匹配(判断Http请求中的信息是否满足一定的条件,以此来匹配相应的路由)。
SpringCloud Gateway内置了很多路由断言工厂,不同的断言工厂对HTTP请求的不同信息进行断言,可以将多个路由断言组合使用。
几种方式:
spring.cloud.gateway.routes[0].predicates[0]=Path=/info/** #请求路径为服务中的/info/**则路由可用 spring.cloud.gateway.routes[0].predicates[1]=After=2020-06-20T17:42:47.789-07:00[Asia/Shanghai] #在此时间点之后路由可用 spring.cloud.gateway.routes[0].predicates[2]=Before=2020-06-18T21:26:26.711+08:00[Asia/Shanghai] #在此时间点之前路由可用 spring.cloud.gateway.routes[0].predicates[3]=Between=2020-06-18T21:26:26.711+08:00[Asia/Shanghai],2020-06-18T21:32:26.711+08:00[Asia/Shanghai] #在这两个时间之间路由可用 spring.cloud.gateway.routes[0].predicates[4]=Cookie=name,xiaobai #请求信息中包含名称为name值为xiaobai的Cookie则路由可用 spring.cloud.gateway.routes[0].predicates[5]=Header=token,123456 #请求信息中包含名称为token值为123456的请求头则路由可用 spring.cloud.gateway.routes[0].predicates[6]=Host=www.hello.com #主机名为www.hello.com则路由可用 spring.cloud.gateway.routes[0].predicates[7]=Method=GET,POST #请求方式为get或post则路由可用 spring.cloud.gateway.routes[0].predicates[8]=Query=username #带有请求参数username则路由可用
过滤器/处理器:
功能:
拦截请求,在请求前后进行处理的作用。,
分类:
- GatewayFilter:局部过滤器,需要配置给某个路由,才能过滤。
- GlobalFilter:全局过滤器,不需要配置路由,系统初始化时会作用到所有路由上。
全局过滤器的配置:
第一步:创建过滤器类,实现GlobalFilter接口和 Ordered接口,并注入ioc容器
@Component public class GateWayFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return null; } @Override public int getOrder() { return 0; } }
重写两个方法
/** * 全局过滤器,所有的url请求都会先到达这里,再访问服务 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取本次访问的request对象 ServerHttpRequest request = exchange.getRequest(); //通过request对象获取本次访问的url String path = request.getPath().value(); //判断本次的路径包含登录路径,因为路径中带有服务名或其他信息,所以使用包含来判断,而不是使用equals来判断 if (path.contains("/login")) { //放行掉登录的请求 return chain.filter(exchange); } //获取请求头中的token;具体token怎么携带,需要与前端确认 String token = request.getHeaders().getFirst("Authorization"); //判断token是否为空串或为null if (!StringUtils.isEmpty(token)) { //因为含有token,则认为已经登录过了 //省略去redis查询token //放行 return chain.filter(exchange); } //为了更直观的观察认证失败后返回到网页的数据,我把我自己写的注释掉了,用别人的 // //没有token的访问,拒绝访问 // //获取响应对象 // ServerHttpResponse response = exchange.getResponse(); // //认证失败 // response.setStatusCode(HttpStatus.UNAUTHORIZED); // //做出响应,返回到浏览器 // return response.setComplete(); /* 没有认证通过: */ //获取响应对象 ServerHttpResponse response = exchange.getResponse(); //添加响应头,设置响应的类型为application/json response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); //响应数据,一个map集合,将它转为json后再发送到客户端 Map<String, Object> data = new HashMap<>(); data.put("code", 401); data.put("msg", "un_authorization"); //利用ObjectMapper来转换 ObjectMapper objMapper = new ObjectMapper(); byte[] bytes = new byte[0]; try { //将map转成json的字节数据 bytes = objMapper.writeValueAsBytes(data); } catch (JsonProcessingException e) { e.printStackTrace(); } //将json的字节数据组装到数据缓冲区 DataBuffer dataBuffer = response.bufferFactory().wrap(bytes); //响应给客户端 return response.writeWith(Mono.just(dataBuffer)); }
//这个方法的作用就是根据返回值来决定在过滤器链中,本过滤器类的执行时机,0最大。不可返回负数 @Override public int getOrder() { return 0; }
自定义局部过滤器,后置处理器:
第一步:创建配置类,并标记@Configuration 注解
创建方法,返回值为RouteLocator对象
package com.xa.config; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; @Configuration public class FilterConfig { //自定义局部过滤器 @Bean public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) { //获取路由构建器 RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); /* (String id, Function<PredicateSpec, AsyncBuilder> fn) 三个参数: 第一个参数:过滤器的id,唯一即可 第二个参数:一个Function接口,需要实现R apply(T t);方法使用lambda来简化实现 */ return routes.route("login-filter-router", t -> { return t //设置监控的url路径 .path("/login","/hello") /* 设置监控的过滤, 但是这个方法的参数也是一个接口,需要实现R apply(T t)方法;使用lambda来简化实现 */ .filters(r -> { /* 设置过滤器; modifyResponseBody(Class<T> inClass, Class<R> outClass, RewriteFunction<T, R> rewriteFunction) 第一个参数:方法的返回值类型 第二个参数:返回到网页的数据类型 第三个参数:依旧一个接口,使用lambda来简化实现、 1.交换机 2.访问完login方法后的返回值 */ return r.modifyResponseBody(String.class, String.class, (exchange, str) -> { //处理login方法返回值的区域 str += "后置过滤器"; //借助Mono.just来返回前一个接口需要的对象 //这个Mono.just返回的数据就是会到达网页的数据了 return Mono.just(str); }); }) //设置监控的服务名,采用lb协议,负载均衡 .uri("lb://user-service"); }).build(); } }
前后端分离场景,使用网关验证token
第一种:使用全局过滤来判断token是否存在,判断是否已经登录
- 客户端发送请求,登录请求/其他请求
- gateway拦截请求,登录请求可以放行,其他请求判断token是否存在
- login请求,根据携带的name/password,查询数据库,存在用户的话,将用户对象,存入redis,key为生成的token;再将token返回到客户端,之后每次访问客户端都携带token来进行访问
- 其他请求,根据携带的token,查询redis,存在则放行,不存在则直接返回到客户端
代码示例:
package com.pn.filter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; 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.StringRedisTemplate; 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.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.List; import java.util.Map; //@Component public class TokenGlobalFilter implements GlobalFilter, Ordered { //注入Redis模板 @Autowired private StringRedisTemplate redisTemplate; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取请求的url接口 ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().value(); //正要做登录认证 if(path.contains("/doLogin")){ //直接放行 return chain.filter(exchange); } /* 获取请求头Authorization的值,即token: 前后端分离项目,前端携带token一般都是通过请求头Authorization,其值token会带前缀bearer ;具体 使用什么方式要和前端妹子确认; */ List<String> authList = request.getHeaders().get("Authorization"); if(!CollectionUtils.isEmpty(authList)){ String token = authList.get(0); if(!StringUtils.isEmpty(token)){ token = token.replaceAll("bearer ", ""); if(redisTemplate.hasKey(token)){ //放行 return chain.filter(exchange); } } } /* 没有认证通过: */ ServerHttpResponse response = exchange.getResponse(); //设置响应的类型为application/json response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); //响应json Map<String, Object> data = new HashMap<>(); data.put("code", 401); data.put("msg", "un_authorization"); ObjectMapper objMapper = new ObjectMapper(); byte[] bytes = new byte[0]; try { //将map转成json的字节数据 bytes = objMapper.writeValueAsBytes(data); } catch (JsonProcessingException e) { e.printStackTrace(); } //将json的字节数据组装到数据缓冲区 DataBuffer dataBuffer = response.bufferFactory().wrap(bytes); //响应给客户端 return response.writeWith(Mono.just(dataBuffer)); } @Override public int getOrder() { return 0; } }
第二种:使用全局过滤+局部过滤判断
- 客户端发送请求,登录请求/其他请求
- gateway拦截请求,全局过滤开始判断,登录请求可以放行,其他请求判断token是否存在
- 配置专属于login的局部后置过滤器,在登录放到执行完毕后,生成token,再返回到后置过滤器的方法中,由后置过滤器的方法,操作redis,插入记录,并设置保存时间,将token再返回到客户端
- 之后的所有请求都由全局过滤来判断redis中是否存在token
代码示例:
package com.pn.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import reactor.core.publisher.Mono; import java.util.concurrent.TimeUnit; @Configuration public class GatewayConfig { //注入Redis模板 @Autowired private StringRedisTemplate redisTemplate; //给login-service服务配置单独的路由 //@Bean public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder){ //创建路由构建器 RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); //配置路由 return routes.route("login-service-router", r -> { return r.path("/doLogin")//url接口的断言 //配置局部过滤器 .filters(t -> { //配置后置过滤器 -- 参数三Lambda表达式的参数二str是/doLogin接口响应的json字符串 return t.modifyResponseBody(String.class, String.class, (exchange, str) -> { JSONObject jsonObj = JSON.parseObject(str); //是认证通过,/doLogin接口响应的保存了token的json字符串 if(jsonObj.containsKey("token")){ String token = jsonObj.getString("token"); Integer expires_in = jsonObj.getInteger("expires_in"); //将token保存到redis redisTemplate.opsForValue().set(token, "", expires_in, TimeUnit.SECONDS); } //不管认证成功还是失败,都要将/doLogin接口响应json字符串再响应给客户端 return Mono.just(str); }); }) .uri("lb://login-service");//路由到的服务的url地址 }).build(); } }
使用gateway实现限流
限流概述
通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:
- **IP限流:**如5s内同一个ip访问超过3次,则限制不让访问,过一段时间才可继续访问。
- **请求量限流:**在一段时间内,请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问;粒度可以细化到一个url、一个服务的请求量的限流。
Gateway限流
Gateway内置了一个局部过滤器RequestRateLimiterGatewayFilterFactory,其结合Redis并通过令牌桶算法已经实现了限流,我们可以直接使用。
令牌桶算法:
此令牌只是作为标记,并不是指token。
思想概述:
所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。
根据限流大小,设置按照一定的速率往桶里添加令牌。
桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝。
请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,
将令牌直接删除
令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足
够的限流。
实现:
第一步:修改配置文件
server: port: 80 #设置Gateway网关端口--为80 spring: application: name: gateway-80 #设置Gateway网关服务名称--为gateway-80 #----------------Redis的配置------------------------------ redis: host: 192.168.9.128 #redis服务器的ip(安装redis的linux的ip) port: 6379 #redis的端口 password: mmy123 #如果redis设置了密码则指定密码 database: 0 #操作redis的数据库的下标 #redis连接池的配置 lettuce: pool: max-active: 10 #设置池中jedis对象的个数 max-idle: 10 #设置池中jedis对象的最大空闲个数 min-idle: 3 #设置池中jedis对象的最小空闲个数 #----------------Gateway网关的配置------------------------- cloud: gateway: enabled: true #开启gateway网关 #配置路由 routes: - id: search-service-router #路由id uri: lb://search-service #设置路由到的服务的uri--lb协议,服务名search-service #路由断言 predicates: - Path=/doSearch #请求路径为服务中的/doSearch则路由可用 #******给该路由配置限流过滤器RequestRateLimiterGatewayFilterFactory****** filters: #过滤器的引用名称,必须是RequestRateLimiter - name: RequestRateLimiter args: #限流的键的解析器的引用名称,通过它可以拿到限流的键(依据) -- ip限流 ##{}内的参数为,第二步将对象注入ioc容器的方法名 key-resolver: '#{@ipKeyResolver}' #令牌桶每秒填充平均速率 redis-rate-limiter.replenishRate: 1 #令牌桶总容量 redis-rate-limiter.burstCapacity: 3 #将Gateway服务注册到注册中心--值为eureka服务端(注册中心)url地址 eureka: client: service-url: defaultZone: http://localhost:8761/eureka
第二步:创建配置类
@Configuration public class RequestRateLimiterConfig { //---------------------过滤器RequestRateLimiterGatewayFilter限流配置------------------------------------------------ /* 配置限流的键(依据) --- bean对象: 接口KeyResolver: Mono<String> | resolve(ServerWebExchange exchange); */ //@Bean public KeyResolver ipKeyResolver(){ //ip限流 return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } /* 使用url进行限流 */ @Bean public KeyResolver urlKeyResolver(){ //url限流 return exchange -> Mono.just(exchange.getRequest().getPath().value()); } }
跨域配置
配置类添加方法:
@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); } }