Spring Cloud Gateway网关
Spring Cloud Gateway
网关:微服务中最边缘的服务,用来做用户和微服务的桥梁
- 没有网关❓:客户端直接访问我们的微服务,会需要在客户端配置很多的ip和端口,如果服务器并发比较大则无法完成负载均衡
- 有网关❓:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的ip:port)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token拦截,权限验证,等等操作…
网关组件:
gatway
,zuul
- gatway是springcloud官方提供的,用于取代
zuul
核心逻辑:路由转发+执行过滤器链
三大核心概念
Route(路由)
和
eureka
结合做动态路由组成:
- 由一个ID、一个URL、一组断言工厂、一组Filter组成
- 如果路由断言成真,说明请求URL和配置路由匹配
Predicate(断言)
其实就是一个返回true,false的表达式
用于匹配信息做路由限制的
Filter(过滤)
Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
- 一个是针对某一个路由的filter(例如对单个接口做限制)
- 一个是针对全局的filter(例如全局效验token)
开始使用
创建两个模块,一个login-service
,一个gateway-server
- login-service:暂时使用springweb依赖即可
- gateway-server:暂时使用gateway依赖即可
pom.xml
中依赖(例如jar)的版本与前期教程版本号一致
先编写一个接口在login-service
LoginController.java:
@RestController
public class LoginController {
@GetMapping("doLogin")
public String doLogin(String name,String pwd){
System.out.println(name+"密码:"+pwd);
String token= UUID.randomUUID().toString();
return token;
}
}
动态路由
- 配置方式的路由
- 代码方式的路由
两者不会冲突
准备依赖
login-service
:
多一个eureka-client依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.pengke</groupId>
<artifactId>login-service02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>login-service02</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
gateway-server
:
也是多一个eureka-client依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
启动类统一添加启动客户端服务注解
@EnableEurekaClient
配置路由
在login-service模块中进行配置文件配置
server:
port: 8081
spring:
application:
name: login-service
eureka:
client:
service-url:
defaultZone: eureka远程服务端地址
registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
instance:
hostname: localhost
instance-id: ${
eureka.instance.hostname}:${
spring.application.name}:${
server.port}
在gateway-server模块中进行配置文件配置
server:
port: 80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true
discovery:
locator:
enabled: true # 开启动态路由
lower-case-service-id: true # 开启服务名称小写
routes:
- id: login-service-route # 这个是路由id,保持唯一即可
uri: http://localhost:8081 # uri:统一资源定位符 url:统一资源标识符
# uri: lb://login-service # 使用lb协议微服务名称做负载均衡
predicates: # 断言是给某个路由设定的
- Path=/doLogin # 匹配规则 只要Path匹配上/doLogin就往uri转发,并且将路径带上
eureka:
client:
service-url:
defaultZone: eureka远程服务端地址
registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
instance:
hostname: localhost
instance-id: ${
eureka.instance.hostname}:${
spring.application.name}:${
server.port}
启动两个服务,在浏览器中输入localhost/doLogin
或者localhost:8081/doLogin
或者localhost/login-service/doLogin
都可实现访问
代码方式的路由:
在
gateway-server
模块中创建config---》RouteConfig
- .route(“路由id”,函数式访问路径.uri(请求根路径)).build()
@Configuration
public class RouteConfig {
/**
* 代码路由
* 和配置形式路由不冲突
* @param builder
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("test-id",r->r.path("/m_xiaozhilei").uri("https://blog.csdn.net"))
.build();
}
}
启动服务浏览器访问
http://localhost/gateway-server/m_xiaozhilei
或者http://localhost/m_xiaozhilei
都可访问到最终请求的页面
这样就用会了动态路由!
断言
在配置文件中配置
predicates
属性
- 断言是给某个路由设定的
主要有以上这些限制
例如:
predicates:
- After=2023-03-22T09:42:49.521+08:00[Asia/Shanghai]
即可实现请求时间在2023-03-22…之后请求才可以访问成功…
过滤器
按生命周期分两种
- pre:在业务逻辑前
- post:在业务逻辑后
按种类分也是两种
- GatewayFilter:需要配置某个路由才能过滤,如果需要使用全局路由,需要配置Defilters
- GlobalFilter:全局过滤器,不需要配置路由,系统初始化作用到所有路由上
在网关gatewayserver
模块创建过滤器
filter=》MyGlobalFilter.java
/**
* 定义过滤器
*/
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤方法
* 过滤器链模式
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//针对请求的过滤
ServerHttpRequest req=exchange.getRequest();
String path=req.getURI().getPath();
HttpHeaders header=req.getHeaders();
String methods=req.getMethod().name();
String host=req.getRemoteAddress().getHostName();
String ip=req.getHeaders().getHost().getHostString();
System.out.println(path+"header="+header+"方法="+methods+"host="+host+"ip="+ip);
//响应的数据
ServerHttpResponse resp=exchange.getResponse();
resp.getHeaders().set("content-type","application/json;charset=utf-8");
Map map=new HashMap();
map.put("code",500);
map.put("msg","token有误");
ObjectMapper objectMapper=new ObjectMapper();
//将map转字节
byte[] bytes=new byte[0];
try {
bytes=objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//通过buffer工厂将字节数组包装成一个数据包
DataBuffer warp=resp.bufferFactory().wrap(bytes);
if(1>0){
//判断一下(例如判断token是否正确)
return resp.writeWith(Mono.just(warp));
}else{
//放行到下一个过滤器
return chain.filter(exchange);
}
}
/**
* 指定顺序的方法
* 越小越先执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
实现
GlobalFilter和Ordered
的方法
- 过滤器中chain.filter(exchange)表示放行
- 过滤器获取相关参数在上方代码已示例
ordered
中getOrder方法用来指定过滤器的执行顺序
通过上方即可实现过滤器,再次访问页面时则会返回{msg:token有误,coed:500}
实现Token+IP验证拦截
首先简单实现一下IP拦截,常用于黑名单,白名单
- 在网关中定义一个IP拦截过滤器
@Component
public class IPcheckFilter implements GlobalFilter, Ordered {
/**
* 网关的并发比较高 不要在网关里直接操作数据库
*/
public static final List<String> BLACK_LIST= Arrays.asList("127.0.0.1","144.125.231.14");
/**
* 拿到ip进行校验决定是否拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request=exchange.getRequest();
String ip=request.getHeaders().getHost().getHostString();
//这里查数据库获取黑名单或者白名单进行相关操作
if(!BLACK_LIST.contains(ip)){
//不是黑名单放行
return chain.filter(exchange);
}
//拦截
ServerHttpResponse response=exchange.getResponse();
response.getHeaders().set("context-type","application/json;charset=utf-8");
HashMap<String,Object> map=new HashMap<>(4);
map.put("code",438);
map.put("msg","黑名单禁止访问");
ObjectMapper objectMapper=new ObjectMapper();
byte[] bytes= new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap=response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return -5;
}
}
代码如上,思路解析:
- 定义一个常量用于存储黑名单ip或者白名单ip,这里我存的是黑名单ip(业务中常常存与临时存储处,这里演示我就定个变量去存储)
- 通过
.contains
进行判断是否存在- 通过
chain.filter(exchange)
进行放行- 如果验证失败则可返相关信息告知访问者
开始实现token拦截验证
这里使用
redis
进行存储token(不会redis
可查阅我另俩篇文章redis的开始使用--------->redis与springboot整合)
引入redis依赖
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在上方doLogin
接口中存储登陆者的信息以及为它生成的token,这里我创了个User
实体类用于模拟真实登陆场景
@Autowired
public StringRedisTemplate redisTemplate;
@GetMapping("doLogin")
public String doLogin(String name,String pwd){
System.out.println(name+"密码:"+pwd);
User user=new User(1,name,pwd,20);
//token
String token= UUID.randomUUID().toString();
//存在radis
redisTemplate.opsForValue().set(token,user.toString(), Duration.ofSeconds(7200));
return token;
}
在网关中编写token拦截接口
//指定放行路径
public static final List<String> ALLOW_URL= Arrays.asList("/login-service/doLogin","/myUrl");
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 约定好请求头携带 Authorization value:bearer token
* - 拿到url效验决定是否需要token效验
* - 拿到请求头
* - 拿到token效验
* - 决定是否放行
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request=exchange.getRequest();
String path=request.getURI().getPath();
if(ALLOW_URL.contains(path)){
return chain.filter(exchange);//放行不进行token效验
}
HttpHeaders headers=request.getHeaders();
List<String> authorization=headers.get("Authorization");
if(!CollectionUtils.isEmpty(authorization)){
//携带authorization了
String token=authorization.get(0);
if(StringUtils.hasText(token)){
//约定好的前缀 bearer token
String realToken=token.replaceFirst("bearer ","");
if(StringUtils.hasText(realToken)&&redisTemplate.hasKey(realToken)){
//真携带了token
return chain.filter(exchange);
}
}
}
//拦截
ServerHttpResponse response=exchange.getResponse();
response.getHeaders().set("context-type","application/json;charset=utf-8");
HashMap<String,Object> map=new HashMap<>(4);
map.put("code",403);
map.put("msg","无权限访问");
ObjectMapper objectMapper=new ObjectMapper();
byte[] bytes= new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap=response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
首先指定放行非需要token的请求,例如登陆接口!!!
大概验证流程为代码中的注释
通过
redisTemplate.hasKey(token)
去匹对是否有该token来决定是否允许访问该接口最终就实现了一个基本的业务拦截需求了~✌
下一篇讲解实现redis限流
!