本节配套案例代码:Gitee仓库、Github仓库
所有博客文件目录索引:博客目录索引(持续更新)
学习视频:动力节点最新SpringCloud视频教程|最适合自学的springcloud+springcloudAlibaba
PS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。
在分布式微服务架构中,某个服务可以会有多个实例来去注册到注册中心,那么如何去调用如此多个的服务也成为了一个比较大的问题。
此时客户端去调用服务就会出现以下问题:①客户端访问地址配置问题。②多个服务的认证授权问题,造成鉴权认证功能重复冗余情况。③服务访问量大造成的重构问题。
如何解决上面的问题呢?微服务引入了 网关 的概念,网关为微服务架构的系统提供简单、有效且统一的API路由管理,作为系统的统一入口,提供内部服务的路由中转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
原本的对应服务客户端去直接调服务实例接口转变为统一走gateway网关来进行服务转发:
没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的 ip:port,如果 user-service 并发比较大,则无法完成负载均衡。
有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的 ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可 以实现 token 拦截,权限验证,限流等操作 。
本章节介绍其中的Gateway:是 Spring Cloud 官方提供的用来取代 zuul(netflix)的新一代网关组件
Zuul 1.0 : Netflix开源的网关,使用Java开发,基于Servlet架构构建,本质就是 web 组件 web 三大组件(监听器 过滤器 servlet)便于二次开发。因为基于Servlet内部延迟严重,并发场景不友好,一个线程只能处理一次连接请求。
Zuul 2.0 : 采用Netty实现异步非阻塞编程模型,一个CPU一个线程,能够处理所有的请求和响应,请求响应的生命周期通过事件和回调进行处理,减少线程数量,开销较小。
GateWay : 是Spring Cloud的一个全新的API网关项目,替换Zuul开发的网关服务,基于Spring5.0 + SpringBoot2.0 + WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发,性能高于Zuul
Nginx+lua : 性能要比上面的强很多,使用Nginx的反向代码和负载均衡实现对API服务器的负载均衡以及高可用,lua作为一款脚本语言,可以编写一些简单的逻辑,但是无法嵌入到微服务架构中。
Kong : 基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,性能高效且稳定,支持多个可用插件(限流、鉴权)等,开箱即可用,只支持HTTP协议,且二次开发扩展难,缺乏更易用的管理和配置方式
Spring Cloud Gateway 是Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul1。
技术选型:基于Spring5.0 + SpringBoot2.0 + WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发。
性能方面:性能⾼于Zuul,官⽅测试,Spring Cloud GateWay是Zuul的1.6倍 ,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式。
特点:Spring Cloud Gateway 里明确的区分了 Router 和 Filter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。
三个核心:路由、断言、过滤器。
路由(Route)
:能够与注册中心来结合作动态路由。是GateWay中最基本的组件之一,表示一个具体的路由信息载体,主要有一个ID、一个目标URI、一组断言和一组过滤器来定义,具体如下:
断言(Predicate)
:返回一个bool类型,用于表示在不同状态情况下,该请求是否符合要求。输入的类型是一个ServerWebExchange,可以使用其来匹配HTTP请求的任何内容,例如headers、cookie等等。
过滤器(filter)
:在gateway中分为两种类型filter:①Gateway filter。②Global filter。
执行流程如下:
1、Gateway Client
向 Spring Cloud Gateway
发送请求,请求首先会被 HttpWebHandlerAdapter
进行提取组装成网关上下文。
3、此时网关的上下文会传递到 DispatcherHandler
,它负责将请求分发给 RoutePredicateHandlerMapping
。
4、RoutePredicateHandlerMapping
负责路由查找,并根据路由断言判断路由是否可用。
5、如果过断言成功,由 FilteringWebHandler
创建过滤器链并调用。
6、通过特定于请求的 Fliter
链运行请求,Filter
被虚线分隔的原因是Filter可以在发送代理请求之前(pre)和之后(post)运行逻辑。
7、执行所有pre过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
8、处理完毕之后将 Response
返回到 Gateway
客户端。
针对于代理请求pre与post的分别应用场景:
若是想要我们的网关达到高可用,那么就需要进行部署集群,并在gateway网关上层配置一个负载均衡层:
Nginx
:在做路由,负载均衡,限流之前,都有修改 nginx.conf 的配置文件,把需要负载均衡,路由,限流的规则加在里面。
gateway
:gateway 自动的负载均衡和路由,gateway 和 eureka 高度集成,实现自动的路由,和 Ribbon 结合,实现了负载均衡(
lb),gateway 也能轻易的实现限流和权限验证。
区别:
1、Nginx需要去自行配置路由、负载均衡等规则;gateway有动态路由,可与注册中心结合使用。
2、Nginx(c语言)比gateway(gateway)的性能高一点。
3、Nginx是服务器级别的;Gateway是项目级别的。
比较合适的搭配效果如下:
项目版本:SpringBoot 2.3.12.RELEASE
、SpringCloud Hoxton.SR12
。
本小节只需要使用一个gateway与一个服务实例即可。
实现目标:网关实现路由转发的效果。(之后小结会进行动态路由的实现)
login-service:登录模块服务
当前该服务仅仅只要引入web模块即可,当前还没有涉及到注册中心。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
controller/LoginController.java
:
package com.changlu.loginservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @Description:
* @Author: changlu
* @Date: 8:20 PM
*/
@RestController
public class LoginController {
@GetMapping("/doLogin")
public String doLogin() {
return UUID.randomUUID().toString();
}
}
gateway-service:网关服务
依赖配置:对应springboot、springcloud版本号在三章节下有说明
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
配置文件:application.yaml
server:
port: ${SERVER_PORT:81} # 默认是81端口,可以通过命令行读取参数 -DSERVER_PORT=82
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
uri: http://localhost:8081 # uri
predicates:
- Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
目前的话我们无需编写任何代码,就可以使用一个路由转发的效果,接下来我们来进行测试一下!
测试:访问路径http://localhost:81/doLogin,即可在gateway中进行转发到我们的login-service服务实例上。
注意:当前仅仅是路由转发指定的地址,并没有去注册中心拉取服务实例进行访问!
可参考官方案例:https://spring.io/projects/spring-cloud-gateway#overview
实现目标:路由转发到百度一下
可以看到百度的路由地址是:https://www.baidu.com/s?wd=123
思路:将/s即之后的内容转发到指定的百度网址,实际上与3.1中配置的大体参数类似
package com.changlu.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;
/**
* @Description:
* @Author: changlu
* @Date: 1:15 PM
*/
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
//路由
.route("baidu_route", r -> r.path("/s").uri("https://www.baidu.com/"))
.build();
}
}
ok,此时我们来测试一下:http://localhost:81/s?wd=123
实际上就是3.1中进行配置的内容:
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
uri: http://localhost:8081 # uri
predicates:
- Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- id: user-service-route # 路由id,保持唯一
uri: http://localhost:8082 # uri
predicates:
- Path=/info/** # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
对于下面的/info/,就是会匹配所有前缀为/info/*
的内容
例如访问http://localhost:81/info/doLogin,实际上就会进行转发http://localhost:8081/info/doLogin
之前3.1中案例并没有搭配注册中心,我们就以3.1中的案例来进行集成实现!
同样是对前两个进行改造:
两个服务都添加如下依赖及配置:
①引入依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
②配置application.yaml添加eureka连接信息:
# 配置eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
③开启服务发现:在启动器类上添加
@EnableEurekaClient //开启服务注册
最后在``gateway-server`服务的yaml配置里进行开启动态路由:
# 服务发现相关配置
discovery:
locator:
enabled: true # 开启动态路由 开启通用应用名称 找到服务的功能
lower-case-service-id: true # 开启服务名称小写,因为在eureka中默认服务名是大写的
当前我们已经实现了动态路由了,完全就只需要进行配置即可!
我们来启动网关、生产者服务以及eureka注册中心:
在对login-service
与gateway-server
都开启了服务注册之后,以及开启了gateway-server
的动态路由,我们就可以来实现根据服务名调用指定注册中心中的服务实例了。
再此之前我们进行路由代码配置访问的是:http://localhost:81/doLogin
针对于动态路由,我们在/doLogin前添加一个服务名,例如在eureka中心中login-service注册的服务名为login-service:http://localhost:81/login-service/doLogin
测试一下:ok能够进行测试访问
之前3.1、3.2节是我们进行静态绑定的,那么我们如何来实现静态绑定的方式来达到动态路由的效果呢?
那么我们只需要对对应uri来进行操作即可:
将原本指明服务ip地址以及port端口的更改为负载均衡协议lb://服务名
# uri: http://localhost:8081 # uri
uri: lb://login-service # 实现负载均衡
测试一下:
没得问题!
目标效果:访问nginx,通过nginx来进行负载均衡转发请求到集群gateway中,此时gateway里同样去搭配注册中心进行服务发现来进行负载均衡访问服务实例!下面就开始吧。
准备:nginx服务器+gateway两个不同端口(实际上是不同ip地址)服务+生产者服务实例。
生产者服务实例:使用的是login-service,也就是3.1节中的对外提供了一个接口。
gateway服务实例准备
直接使用的是3.1章节中的gateway,如何创建不同端口的多实例呢?
我们针对yaml配置文件来编写可接收命令传参:
server:
port: ${SERVER_PORT:81} # 默认是81端口,可以通过命令行读取参数 -DSERVER_PORT=82
-DSERVER_PORT=82
接着我们来进行启动服务实例:
nginx配置
下载地址:http://nginx.org/en/download.html
我们打开nginx文件夹中的nginx.conf来进行编辑:
配置如下:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'
'$connection $upstream_addr '
'upstream_response_time $upstream_response_time request_time $request_time ';
access_log logs/access.log main;
upstream www.gateway.com {
server localhost:81;
server localhost:82;
}
server {
location / {
# root html;
# index index.html index.htm;
# 代理服务地址
proxy_pass http://www.gateway.com;
}
}
nginx相关的启动、关闭命令:
start nginx # 启动,下载好之后到指定目录下执行启动命令即可
nginx -s quit # 关闭nginx
nginx -s reload # 优雅重启
# 若是想要停止服务还可以使用这个命令或者使用任务管理器找到nginx.exe来关闭
taskkill /IM nginx.exe /F # 关闭所有正在启动的nginx服务
接下来我们来访问nginx的80端口:http://localhost/doLogin
那么我们如何来看负载均衡访问服务器的地址呢?
查看nginx的日志:``access.log`即可
可以看到默认的nginx负载均衡是轮训:
注意:默认的日志打印内容是没有访问服务器的地址的,在前面nginx中我是有进行自主配置加上打印的服务器地址这里才会显示的。
核心:Predicate
就是为了实现一组匹配规则,让请求过来找到对应的 Route 进行处理。
在 gateway 启动时会去加载一些路由断言工厂**(判断一句话是否正确 一个 boolean 表达式** ) ,例如我们3.1中搭建的案例在启动时就会出现如下的一些断言信息:
本质:满足条件的返回true放行,不满足的false进行拦截。
介绍:Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分进行匹配。Spring Cloud Gateway 包括许多内置的路由断言工厂。所有这些断言都与 HTTP 请求的不同属性匹配。您可以将多个路由断言可以组合使用。
源码:Spring Cloud Gateway 创建对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true # 默认开启,只要加了网关依赖
routes:
- id: login-service-route # 路由id,保持唯一
# uri: http://localhost:8081 # uri
uri: lb://login-service # 实现负载均衡
predicates:
- Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
- After=2020-01-20T17:42:47.789-07:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之后的请求,ZonedDateTime dateTime=ZonedDateTime.now()获得
- Before=2020-06-18T21:26:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之前的请求
- Between=2020-06-18T21:26:26.711+08:00[Asia/Shanghai],2020-06-18T21:32:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之间的请求
- Cookie=name,xiaobai #Cookie 路由断言工厂接受两个参数,Cookie 名称和 regexp(一 个 Java 正则表达式)。此断言匹配具有给定名称且其值与正则表达式匹配的 cookie
- Header=token,123456 #头路由断言工厂接受两个参数,头名称和 regexp(一个 Java 正 则表达式)。此断言与具有给定名称的头匹配,该头的值与正则表达式匹配。
- Host=**.bai*.com:* #主机路由断言工厂接受一个参数:主机名模式列表。该模式是一 个 ant 样式的模式。作为分隔符。此断言匹配与模式匹配的主机头
- Method=GET,POST #方法路由断言工厂接受一个方法参数,该参数是一个或多个参数: 要匹配的 HTTP 方法
- Query=username,cxs #查询路由断言工厂接受两个参数:一个必需的 param 和一个 可选的 regexp(一个 Java 正则表达式)。
- RemoteAddr=192.168.1.1/24 #RemoteAddr 路由断言工厂接受一个源列表(最小大小 1), 这些源是 cidr 符号(IPv4 或 IPv6)字符串,比如 192.168.1.1/24(其中 192.168.1.1 是 IP 地址,24 是子网掩码)。
其他额外的包含有权重属性:下面
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 2 # 权重
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 8 # 权重
效果:指定某个接口只能在指定时间后才能够进行访问,否则无法访问,报出404异常。
配置内容如下:- After=2022-07-29T17:23:21.719+08:00[Asia/Shanghai]
可以看到当前时间是四点多,配置After是五点多,也就是说这个接口在五点多才能够访问,再此之前不能够访问!
测试效果:
自定义步骤
1、编写一个断言工厂类:注意工厂类的名字尽量为xxxRoutePredicateFactory
,因为之后配置文件要进行配置
package com.changlu.config;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.function.Predicate;
/**
* @Description:
* @Author: changlu
* @Date: 4:38 PM
*/
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
public CheckAuthRoutePredicateFactory() {
super(Config.class);
}
//此时Config也就是自定义配置的一些参数
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
System.out.println("当前进入到CheckAuthRoutePredicateFactory:" + config.getName());
return config.getName().equals("changlu");
};
}
@Data
static class Config {
private String name;
}
}
2、yaml
来进行配置
- name: CheckAuth #自定义路由断言工厂的名称xxxRoutePredicateFactory,这个xxx就是在这里指明
args:
name: changlu1 #传入到自定义路由断言工厂的参数
测试
果然由于配置文件的name与在断言方法类中的值不一致,此时该接口就访问不到了
将name修改为changlu,再次尝试一下:
介绍:gateway 里面的过滤器和 Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http 请求和返回Http响应。
分类:
按照生命周期:pre(在业务逻辑前)、post(在业务逻辑后)。
按照种类区别:路由过滤器(某个路由单独使用)、全局过滤器(所有路由)。
官网
官网-gatewayfilter-factories:包含31中单一路由过滤器。
官网-global-filters:包含9种全局路由过滤器。
自定义过程
我们基于之前的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请求对象的信息:
思路:同样也是在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会进行拦截响应:
在实战5中,我们完成的就是下图的第7步骤,也就是token进行认证校验是否合法来进行放行或直接响应!
说明:本章节的话会在login-service中完善doLogin接口,接着在gateway服务里添加一个认证token过滤器,并新建一个user-service并在其中添加一个接口对外使用。
注意:本章节的重点是在gateway中实现token认证来达到放行or错误响应,并不是在登录接口存储用户信息这些细节上,对于token生成、校验以及用户认证都仅仅只是做了简单的实现。
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;
}
说明:该模块主要是用于测试之后携带token的接口是否能够通过gateway认证并进行转发。
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.12.RELEASEversion>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR12spring-cloud.version>
properties>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
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;
}
}
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来发送一下:
通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:
介绍限流模型
限流模型:漏斗算法,令牌桶算法,窗口滑动算法,计数器算法。
常用的模型分类有两种:
本章实战说明
本章节的话就使用Gateway内置的一个限流过滤器RequestRateLimiterGatewayFilterFactory
:
也就是令牌桶限流模型:入不敷出
1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)、根据限流大小,设置按照一定的速率往桶里添加令牌;
3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完
业务逻辑之后,将令牌直接删除;
5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令
牌,以此保证足够的限流;
集成过程
注意:Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,该过滤器是针对于某个路由的,并不是全局过滤器。
1、添加redis依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
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中存储的如下:
对于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>Documenttitle>
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>
配置完跨域后再来进行测试:
[1]. 如何设置nginx日志格式来查看负载分担结果
[2]. Nginx负载均衡配置+记录请求分发日志
[3]. Spring Cloud Gateway-自定义断言及过滤器
[4]. spring-cloud-gateway 11 限流 RequestRateLimiterGatewayFilterFactory