正文
六、动态网关
动态网管可以使用配置中心,Redis,和数据库(我用的方法)
直接上代码,其实就是增删改查
package com.xiaojie.entity; import lombok.Data; /** * @Description: 路由实体类 * @author: xiaojie * @date: 2021.07.15 */ @Data public class Route { private Integer id; private String routeId; //路由id private String routeName;//路由名称 private String routePattern;//路由转发地址, private Integer routeType;//路由类型1从注册中心获取,0-转发地址 private String routeUrl;//转发到的地址 private Integer isOpen;//是否开放0-否1-开放 private Integer isUse;//是否可用0-否1-开放 }
package com.xiaojie.mapper; import com.xiaojie.entity.Route; import org.apache.ibatis.annotations.*; import java.util.List; /** * @Description:Mapper接口 * @author: xiaojie * @date: 2021.07.15 */ public interface RouteMapper { @Select("SELECT id,routeId,routeName,routePattern,routeType,routeUrl,is_open isOpen,is_use isUse FROM `tb_route` WHERE is_open=1 and is_use=1;") List<Route> selectAllRoute(); @Insert("INSERT INTO tb_route(routeId,routeName,routePattern,routeType,routeUrl,is_open,is_use) " + "VALUES(#{routeId},#{routeName},#{routePattern},#{routeType},#{routeUrl},#{isOpen},#{isUse})") @Options(useGeneratedKeys = true, keyProperty = "id") Integer add(Route route); @Update("UPDATE tb_route set routePattern=#{routePattern},routeUrl=#{routeUrl} WHERE routeId=#{routeId}") Integer update(Route route); @Delete("DELETE FROM tb_route WHERE routeId=#{routeId}") Integer delete(String routeId); }
package com.xiaojie.service.impl; import com.xiaojie.entity.Route; import com.xiaojie.mapper.RouteMapper; import com.xiaojie.service.RouteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Description: * @author: xiaojie * @date: 2021.07.15 */ @Service @Slf4j public class RouteServiceImpl implements RouteService, ApplicationEventPublisherAware { private ApplicationEventPublisher applicationEventPublisher; @Autowired private RouteMapper routeMapper; @Autowired private RouteDefinitionWriter routeDefinitionWriter; @Override public List<Route> getAll() { List<Route> routes = routeMapper.selectAllRoute(); for (Route route: routes){ loadRoute(route); } return routes; } public void loadRoute(Route route) { RouteDefinition definition = new RouteDefinition(); Map<String, String> predicateParams = new HashMap<>(8); PredicateDefinition predicate = new PredicateDefinition(); FilterDefinition filterDefinition = new FilterDefinition(); Map<String, String> filterParams = new HashMap<>(8); URI uri = null; if (1==route.getRouteType()) { // 如果配置路由type为1的话 则从注册中心获取服务地址 uri = UriComponentsBuilder.fromUriString(route.getRouteUrl()).build().toUri(); } else { uri = UriComponentsBuilder.fromHttpUrl(route.getRouteUrl()).build().toUri(); } // 定义的路由唯一的id definition.setId(route.getRouteId()); predicate.setName("Path"); //路由转发地址 predicateParams.put("pattern", route.getRoutePattern()); predicate.setArgs(predicateParams); filterDefinition.setName("StripPrefix"); filterParams.put("_genkey_0", "1"); filterDefinition.setArgs(filterParams); definition.setPredicates(Arrays.asList(predicate)); definition.setFilters(Arrays.asList(filterDefinition)); definition.setUri(uri); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this)); log.info("路由刷新成功............................."); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher=applicationEventPublisher; } }
定义接口,当修改路由之后,需要刷新路由规则
@RequestMapping("/refreshGw") public Object refreshGw() { return routeService.getAll(); }
SQL如下
/* Navicat MySQL Data Transfer Source Server : 本地 Source Server Type : MySQL Source Server Version : 80024 Source Host : localhost:3306 Source Schema : my_test Target Server Type : MySQL Target Server Version : 80024 File Encoding : 65001 Date: 15/07/2021 15:25:24 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for tb_route -- ---------------------------- DROP TABLE IF EXISTS `tb_route`; CREATE TABLE `tb_route` ( `id` int NOT NULL AUTO_INCREMENT, `routeId` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '路由id', `routeName` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '路由名称', `routePattern` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '路由转发地址', `routeType` tinyint NULL DEFAULT NULL COMMENT '路由类型1从注册中心获取,0-转发地址', `routeUrl` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '转发到的地址', `is_open` tinyint NULL DEFAULT NULL COMMENT '是否开放0-否1-开放', `is_use` tinyint NULL DEFAULT NULL COMMENT '是否可用0-否1-开放', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '动态网关表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of tb_route -- ---------------------------- INSERT INTO `tb_route` VALUES (1, 'member', 'xiaojie-member', '/member/**', 1, 'lb://xiaojie-member/', 1, 1); INSERT INTO `tb_route` VALUES (3, 'baidu', 'xiaojie-baidu', '/bai/**', 0, 'http://www.baidu.com', 1, 1); SET FOREIGN_KEY_CHECKS = 1;
配置文件如下
server: port: 82 ####服务网关名称 spring: application: name: xiaojie-gateway cloud: gateway: ###路由策略 discovery: locator: enabled: true nacos: discovery: #注册地址 server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 #是否开启nacos注册 enabled: true #账号 username: xiaojie #密码 password: nacos #命名空间 namespace: 28eb29ea-5a04-4714-8ae8-77d37c01166a #分组 group: DEV_GROUP datasource: driver-class-name: com.mysql.cj.jdbc.Driver filters: stat initialSize: 10 maxActive: 50 maxOpenPreparedStatements: 20 maxWait: 60000 minEvictableIdleTimeMillis: 300000 minIdle: 10 password: root poolPreparedStatements: true testOnBorrow: false testOnReturn: false testWhileIdle: true timeBetweenEvictionRunsMillis: 60000 type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&autoReconnect=true username: root validationQuery: select 1
七、网关解决跨域
package com.xiaojie.filter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.HttpHeaders; 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; /** * @Description: 解决微服务的跨域问题 * @Author: yan * @Date: 2021/5/12 23:18 * @return: null **/ @Component public class CrossOriginFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); HttpHeaders headers = response.getHeaders(); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "POST, GET, PUT, OPTIONS, DELETE, PATCH"); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*"); headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); return chain.filter(exchange); } }
八、网关实现限流
使用的算法是令牌桶 Redis+Lua
令牌流与令牌桶
系统会以一定的速度生成令牌,并将其放置到令牌桶中,可以将令牌桶想象成一个缓冲区(可以用队列这种数据结构来实现),当缓冲区填满的时候,新生成的令牌会被扔掉。 这里有两个变量很重要:
第一个是生成令牌的速度,一般称为 rate 。比如,我们设定 rate = 2 ,即每秒钟生成 2 个令牌,也就是每 1/2 秒生成一个令牌;
第二个是令牌桶的大小,一般称为 burst 。比如,我们设定 burst = 10 ,即令牌桶最大只能容纳 10 个令牌。
数据流
数据流是真正的进入系统的流量,对于接口来讲,如果平均每秒钟会调用2次,则认为速率为 2次/s 。
算法原理
系统接收到一个单位数据(对于网络传输,可以是一个包或者一个字节;对于微服务,可以是一个请求)后,从令牌桶中取出一个令牌,然后对数据或请求进行处理。如果令牌桶中没有令牌了,会直接将数据或者请求丢弃。当然,对于微服务,就不能是丢弃这么简单了:可以返回一个异常消息,用于提示用户其请求速率超过了系统限制。
有以下三种情形可能发生:
数据流的速率 等于 令牌流的速率。这种情况下,每个到来的数据包或者请求都能对应一个令牌,然后无延迟地通过队列;
数据流的速率 小于 令牌流的速率。通过队列的数据包或者请求只消耗了一部分令牌,剩下的令牌会在令牌桶里积累下来,直到桶被装满。剩下的令牌可以在突发请求的时候消耗掉。
数据流的速率 大于 令牌流的速率。这意味着桶里的令牌很快就会被耗尽。导致服务中断一段时间,如果数据包或者请求持续到来,将发生丢包或者拒绝响应。
比如前面举的例子,生成令牌的速率和令牌桶的大小分别为 rate = 2, burst = 10 ,则系统能承受的突发请求速率为 10次/s ,平均请求速率为 2次/s 。
摘自:https://blog.csdn.net/xgw1010/article/details/107595141
代码如下
package com.xiaojie.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; /** * @Description: * @author: xiaojie * @date: 2021.07.16 */ @Configuration public class MyKeyResolver { /* * ip限流 每一个ip只能在限流的规则内访问的规则,超出规则,进行限流 * @todo * @author xiaojie * @date 2021/7/16 9:44 * @return org.springframework.cloud.gateway.filter.ratelimit.KeyResolver */ // @Bean("ipKeyResolver") // @Primary // public KeyResolver ipKeyResolver() { // return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostString()); // } /* * 用户限流 每一个用户的限流规则 访问时需要携带userId 参数 * @todo * @author xiaojie * @date 2021/7/16 9:44 * @return org.springframework.cloud.gateway.filter.ratelimit.KeyResolver */ @Bean("userKeyResolver") KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); } /* * 接口限流 根据接口路径限流 * @todo * @author xiaojie * @date 2021/7/16 9:45 * @return org.springframework.cloud.gateway.filter.ratelimit.KeyResolver */ // @Bean("apiKeyResolver") // KeyResolver apiKeyResolver() { // return exchange -> Mono.just(exchange.getRequest().getPath().value()); // } }
路由配置如下
server: port: 82 ####服务网关名称 spring: application: name: xiaojie-gateway cloud: gateway: ###路由策略 # routes: # ###根据我们的服务名称查找地址实现调用 # - id: member # #uri: http://127.0.0.1:8090 # uri: lb://xiaojie-member/ # filters: # - StripPrefix=1 # ###匹配规则 # predicates: # - Path=/member/* routes: - id: requestratelimiter_route uri: lb://xiaojie-member/ filters: - name: RequestRateLimiter args: #允许用户每秒执行多少请求,而没有任何丢弃的请求。这是令牌桶填充的速率 redis-rate-limiter.replenishRate: 1 #允许用户在一秒内执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零会阻止所有请求。 redis-rate-limiter.burstCapacity: 1 #每个请求从存储桶中获取的令牌数量,默认为 1 redis-rate-limiter.requestedTokens: 1 key-resolver: - "#{@userKeyResolver}" #根据用户限流 - StripPrefix=1 predicates: - Path=/member/* #从注册中心获取服务 discovery: locator: enabled: true nacos: discovery: #注册地址 server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 #是否开启nacos注册 enabled: true #账号 username: xiaojie #密码 password: nacos #命名空间 namespace: 28eb29ea-5a04-4714-8ae8-77d37c01166a #分组 group: DEV_GROUP datasource: driver-class-name: com.mysql.cj.jdbc.Driver filters: stat initialSize: 10 maxActive: 50 maxOpenPreparedStatements: 20 maxWait: 60000 minEvictableIdleTimeMillis: 300000 minIdle: 10 password: root poolPreparedStatements: true testOnBorrow: false testOnReturn: false testWhileIdle: true timeBetweenEvictionRunsMillis: 60000 type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&autoReconnect=true username: root validationQuery: select 1 #基于redis实现限流 redis: host: 192.168.6.130 password: xiaojie port: 6379
限流后返回429状态码
动态限流 基于Sentinel
九、设置黑白名单
package com.xiaojie.filter; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; /** * @Description: 黑名单 * @author: xiaojie * @date: 2021.07.16 */ @Component @Slf4j public class IpFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String ipAddress = getIpAddress(request); log.info("获取的IP地址为{}",ipAddress); //实际生产可以基于redis,数据库实现记录 if ("127.0.0.1".equals(ipAddress)){ ServerHttpResponse response = exchange.getResponse(); HttpHeaders httpHeaders = response.getHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); String msg="已被加入黑名单"; DataBuffer buffer = response.bufferFactory().wrap(msg.getBytes()); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } /* * 获取用户真实ip * @todo * @author xiaojie * @date 2021/7/16 14:52 * @return java.lang.String */ public static String getIpAddress(ServerHttpRequest request) { HttpHeaders headers = request.getHeaders(); String ip = headers.getFirst("x-forwarded-for"); if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { // 多次反向代理后会有多个ip值,第一个ip才是真实ip if (ip.indexOf(",") != -1) { ip = ip.split(",")[0]; } } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = headers.getFirst("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = headers.getFirst("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = headers.getFirst("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = headers.getFirst("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = headers.getFirst("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddress().getAddress().getHostAddress(); } return ip; } }
参考:https://blog.csdn.net/lizz861109/article/details/108530364;
https://blog.csdn.net/qq_32652767/article/details/112257082;
https://blog.csdn.net/xgw1010/article/details/107595141
感谢几位大佬