09、SpringCloud之Gateway网关组件学习笔记(下)
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>
配置完跨域后再来进行测试:
参考文章
[2]. Nginx负载均衡配置+记录请求分发日志
[3]. Spring Cloud Gateway-自定义断言及过滤器
[4]. spring-cloud-gateway 11 限流 RequestRateLimiterGatewayFilterFactory
- 点赞
- 收藏
- 关注作者
评论(0)