spring cloud alibaba开发笔记五(Gateway)

SpringCloud Gateway

◆SpringCloud Gateway是Spring官方最新推出的一款基于SpringFramework 5 ,Project Reactor和SpringBoot 2之上开发的网关

◆它与第- -代网关Zuul不同的是: gateway是异步非阻塞的( netty + webflux实现) ;zuul是同步阻塞请求的

◆Gateway 三大组成部分:

1.Route路由:ID、目标URL

2.Predicate 断言

3.Filter 过滤器

SpringCloud Gateway工作模型图示及解读:

◆请求发送到网关,经由分发器将请求匹配到相应的HandlerMapping

◆请求和处理器之间有一个映射,路由到网关处理程序,即Web Handler

◆执行特定的请求过滤器链

◆最终到达代理的微服务

创建gateway子模块

pom示例

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>e-commerce-springcloud</artifactId>
        <groupId>com.taluohui.ecommerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>e-commerce-gateway</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>e-commerce-gateway</name>
    <description>Spring Cloud Gateway</description>

    <dependencies>
        <!-- spring cloud alibaba nacos discovery 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.5.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.taluohui.ecommerce</groupId>
            <artifactId>e-commerce-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!--
        SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
        SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
     -->
    <build>
        <finalName>${artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

配置文件bootstrap.yml

server:
  port: 9000
  servlet:
    context-path: /shuai

spring:
  application:
    name: e-commerce-gateway
  cloud:
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 1.15.247.9:8848 # Nacos 服务器地址
        namespace: 22d40198-8462-499d-a7fe-dbb2da958648
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
  main:
    allow-bean-definition-overriding: true  # 因为将来会引入很多依赖, 难免有重名的 bean

# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:
  gateway:
    route:
      config:
        data-id: e-commerce-gateway-router
        group: e-commerce

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
启动类,网关启动入口
/**
 * <h1>网关启动入口</h1>
 */
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

Java8 Predicate 使用方法与思想

/**
 * <h1>Java8 Predicate 使用方法与思想</h1>
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class PredicateTest {

    public static List<String> MICRO_SERVICE = Arrays.asList(
            "nacos", "authority", "gateway", "ribbon", "feign", "hystrix", "e-commerce"
    );

    /**
     * <h2>test 方法主要用于参数符不符合规则,返回值是Boolean</h2>
     */
    @Test
    public void testPredicateTest() {
        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
    }

    /**
     * <h2>and 方法等同于我们的逻辑与 &&, 存在短路特性,需要所有条件都满足</h2>
     */
    @Test
    public void testPredicateAnd() {
        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        Predicate<String> letterStartWith = s -> s.startsWith("gate");
        MICRO_SERVICE.stream().filter(
                letterLengthLimit.and(letterStartWith)
        ).forEach(System.out::println);
    }

    /**
     * <h2>or 方法等同于我们的逻辑或 ||, 多个条件只要一个满足</h2>
     */
    @Test
    public void testPredicateOr() {
        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        Predicate<String> letterStartWith = s -> s.startsWith("gate");
        MICRO_SERVICE.stream().filter(
                letterLengthLimit.or(letterStartWith)
        ).forEach(System.out::println);
    }

    /**
     * <h2>negate 方法等同于我们的逻辑非 !</h2>
     */
    @Test
    public void testPredicateNegate() {
        Predicate<String> letterStartWith = s -> s.startsWith("gate");
        MICRO_SERVICE.stream().filter(letterStartWith.negate()).forEach(System.out::println);
    }

    /**
     * <h2>isEqual 类似于equals(),区别在于:isEqual会先判断对象是否为NULL,
     * 不为NULL再使用equals进行比较</h2>
     */
    @Test
    public void testPredicateIsEqual() {
        Predicate<String> equalGateway = s -> Predicate.isEqual("gateway").test(s);
        MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
    }
}

集成AlibabaNacos实现动态路由配置

静态路由配置

静态路由配置,写在配置文件中端点:spring.cloud.gateway

缺点: 每次改动都需要网关模块重新部署

  # 静态路由
      gateway:
        routes:
          - id: path_route # 路由的ID
            uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
            predicates: # 断言, 路径相匹配的进行路由
              - Path=/user/{id}

动态路由配置

在nacos界面的配置列表进行配置的添加:

 选择JSON格式,示例配置内容,这是nacos-client微服务部分的配置,以后的配置可以模仿这个。

[
    {
        "id": "e-commerce-nacos-client",
        "predicayes": [
            {
                "args": {
                    "pattern": "/shuai/ecommerce-nacos-client/**"
                },
                "name": "Path"
            }
        ],
        "uri": "lb://e-commerce-nacos-client",
        "filters": [
            {
                "name": "HeaderToken"
            },
            {
                "name": "StripPrefix",
                "args": {
                    "parts": "1"
                }
            }
        ]
    }
]

配置类,读取Nacos相关的配置项

/**
 * <h1>配置类,读取Nacos相关的配置项,用于配置监听器</h1>
 */
@Configuration
public class GatewayConfig {

    /** 读取配置的超时时间 */
    public static final long DEFAULT_TIMEOUT = 30000;

    /** Nacos 服务器地址 */
    public static String NACOS_SERVER_ADDR;

    /** 命名空间 */
    public static String NACOS_NAMESPACE;

    /** Data-id */
    public static String NACOS_ROUTE_DATA_ID;

    /** 分组 id */
    public static String NACOS_ROUTE_GROUP;

    @Value("${spring.cloud.nacos.discovery.server-addr}")
    public void setNacosServerAddr(String nacosServerAddr) {
        NACOS_SERVER_ADDR = nacosServerAddr;
    }

    @Value("${spring.cloud.nacos.discovery.namespace}")
    public void setNacosNamespace(String nacosNamespace) {
        NACOS_NAMESPACE = nacosNamespace;
    }

    @Value("${nacos.gateway.route.config.data-id}")
    public void setNacosRouteDataId(String nacosRouteDataId) {
        NACOS_ROUTE_DATA_ID = nacosRouteDataId;
    }

    @Value("${nacos.gateway.route.config.group}")
    public void setNacosRouteGroup(String nacosRouteGroup) {
        NACOS_ROUTE_GROUP = nacosRouteGroup;
    }
}

事件推送 Aware:动态更新路由网关 Service

/**
 * 事件推送 Aware:动态更新路由网关 Service
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    /** 写路由定义 */
    private final RouteDefinitionWriter routeDefinitionWriter;

    /** 获取路由定义 */
    private final RouteDefinitionLocator routeDefinitionLocator;

    /** 事件发布 */
    private ApplicationEventPublisher publisher;

    public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter, RouteDefinitionLocator routeDefinitionLocator) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator = routeDefinitionLocator;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        //完成事件推送句柄的初始化
        this.publisher = applicationEventPublisher;
    }

    /**
     *<h2>增加路由定义</h2>
     */
    public String addRouteDefinition(RouteDefinition definition) {
        log.info("gateway add route: [{}]", definition);

        //保存路由配置并发布
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        //发布事件通知给Gateway,同步新增的路由定义
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }

    /**
     *<h2>更新路由</h2>
     */
    public String updateList(List<RouteDefinition> definitions) {
        log.info("gateway update route: [{}]", definitions);

        //先拿到当前 Gateway  中存储的路由定义
        List<RouteDefinition> routefinitionsExits =
                routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        if(!CollectionUtils.isEmpty(routefinitionsExits)) {
            //清除之前所有的“旧的”路由定义
            routefinitionsExits.forEach(rd -> {
                log.info("delete route definition: [{}]", rd);
                deleteById(rd.getId());
            });
        }
        //把更新的路由定义同步到 gateway 中
        definitions.forEach(definition -> updateByRouteDefinition(definition));
        return "success";
    }

    /**
     *<h2>根据路由 id 删除路由配置</h2>
     */
    private String deleteById(String id) {

        try {
            log.info("gateway delete route id: [{}]", id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            //发布事件通知给Gateway,更新路由定义
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception ex) {
            log.error("gateway delete route fail: [{}]", ex.getMessage(), ex);
            return "delete fail";
        }
    }

    /**
     *<h2>更新路由</h2>
     * 更新路由就是: 删除 + 新增
     */
    private String updateByRouteDefinition(RouteDefinition definition) {
        try {
            log.info("gateway update route: [{}]", definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        } catch (Exception ex) {
            return "update fail, not find route routeId: " + definition.getId();
        }

        try {
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            //发布事件通知给Gateway,更新路由定义
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception ex) {
            return "update route find";
        }
    }
}

通过 nacos 下发动态路由配置,监听 Nacos 中路由配置变更

/**
 * <h1>通过 nacos 下发动态路由配置,监听 Nacos 中路由配置变更</h1>
 */
@Slf4j
@Component
/** 依赖gatewayConfig类,在其加载之后加载 */
@DependsOn({"gatewayConfig"})
public class DynamicRouteServiceImplByNacos {

    /** Nacos 配置服务 */
    private ConfigService configService;
    private final DynamicRouteServiceImpl dynamicRouteService;

    public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
        this.dynamicRouteService = dynamicRouteService;
    }

    /**
     * <h2>Bean 在容器中构造完成之后会执行 init 方法</h2>
     */
    @PostConstruct
    public void init() {
        log.info("gateway route init....");

        try {
            //初始化 Nacos 配置客户端
            configService = initConfigService();
            if(null == configService) {
                log.error("init config service fail");
                return;
            }

            //通过 Nacos Config 并指定路由配置路径去获取路由配置
            String configInfo = configService.getConfig(
                    GatewayConfig.NACOS_ROUTE_DATA_ID,
                    GatewayConfig.NACOS_ROUTE_GROUP,
                    GatewayConfig.DEFAULT_TIMEOUT
            );

            log.info("get current gateway config: [{}]", configInfo);
            List<RouteDefinition> definitions =
                    JSON.parseArray(configInfo, RouteDefinition.class);

            if(CollectionUtils.isNotEmpty(definitions)) {
                for (RouteDefinition definition : definitions) {
                    log.info("init gateway config: [{}]", definition.toString());
                    dynamicRouteService.addRouteDefinition(definition);
                }
            }
        } catch (Exception ex) {
            log.error("gateway route init has some error: [{}]", ex.getMessage(), ex);
        }

        //设置监听器
        dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
                GatewayConfig.NACOS_ROUTE_GROUP);
    }

    /**
     * <h2> 初始化 Nacos Config </h2>
     */
    private ConfigService initConfigService() {
        try {
            Properties properties = new Properties();
            properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
            properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
            return configService = NacosFactory.createConfigService(properties);
        } catch (Exception ex) {
            log.error("init gateway nacos config error: [{}]", ex.getMessage(), ex);
            return null;
        }
    }

    /**
     * <h2>监听 Nacos下发的动态路由配置</h2>
     */
    private void dynamicRouteByNacosListener(String dataId, String group) {
        try {
            // 给 Nacos Config 客户端增加一个监听器
            configService.addListener(dataId, group, new Listener() {
                /**
                 * <h2>自己提供线程池执行操作,不提供使用默认就行</h2>
                 */
                @Override
                public Executor getExecutor() {
                    return null;
                }

                /**
                 * <h2>监听器收到配置更新</h2>
                 * @param configInfo Nacos 中最新的配置定义
                 */
                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("start to update config: [{}]", configInfo);
                    List<RouteDefinition> definitions = JSON.parseArray(configInfo, RouteDefinition.class);
                    log.info("update route: [{}]", definitions.toString());
                    dynamicRouteService.updateList(definitions);
                }
            });
        } catch (NacosException ex) {
            log.error("dynamic update gateway config error: [{}]", ex.getMessage(), ex);
        }
    }
}

最后启动,可以成功读取到nacos上的配置,并且在nacos上更改配置之后,gateway会自动更新配置。

SpringCloud Gateway Filter过滤器

◆SpringCloud Gateway基于过滤器实现,同zuul类似,有pre和post两种方式的filter ,分别处理前置逻辑和后置逻辑

◆客户端的请求先经过 pre类型的filter,然后将请求转发到具体的业务服务 ,收到业务服务的响应之后,再经过post类型的filter处理,最后返回响应到客户端

◆Filter -共有两大类:全局过滤器和局部过滤器

全局过滤器:RouteToRequestUrlFilter   

                        实现接口GlobalFilter,Ordered

局部过滤器:PrefixPathGatewayFilterFactory(前缀局部过滤器)增加相应的前缀  

                      StripPrefixGatewayFilterFactory(后缀局部过滤器)去掉相应的前缀

                        实现接口GatewayFilter,Ordered

HTTP 请求头部携带 Token 验证过滤器(局部过滤器)

新建filter文件夹,在里面添加,这只是演示,并不具备实际项目意义

/**
 * <h1>HTTP 请求头部携带 Token 验证过滤器</h1>
 */
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //从 HTTP Header 中寻找 key 为 token, value 为 imooc 的键值对
        String name = exchange.getRequest().getHeaders().getFirst("token");
        if("shuai".equals(name)) {
            return chain.filter(exchange);
        }

        //标记此次请求没有权限,并结束这次请求
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }
}

由于是局部过滤器,还要实现filter/factory/HeaderTokenAbstractGatewayFilterFactory工厂类

@Component
public class HeaderTokenAbstractGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    @Override
    public GatewayFilter apply(Object config) {
        return new HeaderTokenGatewayFilter();
    }
}

最后还需要在配置文件中进行配置,才会生效。如下在nacos配置管理中的配置

        "filters": [
            {
                "name": "HeaderToken"
            },
            {
                "name": "StripPrefix",
                "args": {
                    "parts": "1"
                }
            }

缓存请求 body 的全局过滤器(Spring WebFlux)

网关常量定义,新建constant文件夹

/**
 * <h1>网关常量定义</h1>
 */
public class GatewayConstant {

    /** 登录 uri */
    public static final String LOGIN_URI = "/e-commerce/login";

    /** 注册 uri */
    public static final String REGISTER_URI = "/e-commerce/register";

    /** 去授权中心拿到登录 token 的 uri 格式化接口 */
    public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/token";

    /** 去授权中心注册拿到 token 的 uri 格式化接口 */
    public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/register";

}

将请求里的body缓存下来,方便后面使用,在filter文件夹下

/**
 * <h1>缓存请求 body 的全局过滤器</h1>
 * Spring WebFlux
 */
@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        boolean isloginOrRegister =
                exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
                || exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);

        if(null == exchange.getRequest().getHeaders().getContentType() || !isloginOrRegister) {
            return chain.filter(exchange);
        }

        //DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {

            //确保数据缓冲区不被释放,必须要DataBufferUtils.retain
            DataBufferUtils.retain(dataBuffer);
            //defer、just都是去创建数据源, 得到当前数据的副本
            Flux<DataBuffer> cachedFlux = Flux.defer(() ->
                    Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            //重新包装 ServerHttpRequest,重写getBody方法, 能够返回请求数据
            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }
            };

            //将包装之后的 ServerHttpRequest 向下继续传递
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        });
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 1;
    }
}

登录,注册,鉴权全局过滤器

在filter文件中新建过滤器

/**
 * <h1>全局登录鉴权过滤器</h1>
 * */
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {

    /** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
    private final LoadBalancerClient loadBalancerClient;
    private final RestTemplate restTemplate;

    public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
                                       RestTemplate restTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
    }

    /**
     * <h2>登录、注册、鉴权</h2>
     * 1. 如果是登录或注册, 则去授权中心拿到 Token 并返回给客户端
     * 2. 如果是访问其他的服务, 则鉴权, 没有权限返回 401
     * */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 1. 如果是登录
        if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
            // 去授权中心拿 token
            String token = getTokenFromAuthorityCenter(
                    request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
            );
            // header 中不能设置 null
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        // 2. 如果是注册
        if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
            // 去授权中心拿 token: 先创建用户, 再返回 Token
            String token = getTokenFromAuthorityCenter(
                    request,
                    GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
            );
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        // 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo = null;

        try {
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch (Exception ex) {
            log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
        }

        // 获取不到登录用户信息, 返回 401
        if (null == loginUserInfo) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        // 解析通过, 则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * <h2>从授权中心获取 Token</h2>
     * */
    private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {

        // service id 就是服务名字, 负载均衡
        ServiceInstance serviceInstance = loadBalancerClient.choose(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );
        log.info("Nacos Client Info: [{}], [{}], [{}]",
                serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
                JSON.toJSONString(serviceInstance.getMetadata()));

        String requestUrl = String.format(
                uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
        );
        UsernameAndPassword requestBody = JSON.parseObject(
                parseBodyFromRequest(request), UsernameAndPassword.class
        );
        log.info("login request url and body: [{}], [{}]", requestUrl,
                JSON.toJSONString(requestBody));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(requestBody), headers),
                JwtToken.class
        );

        if (null != token) {
            return token.getToken();
        }

        return null;
    }

    /**
     * <h2>从 Post 请求中获取到请求数据</h2>
     * */
    private String parseBodyFromRequest(ServerHttpRequest request) {

        // 获取请求体
        Flux<DataBuffer> body = request.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();

        // 订阅缓冲区去消费请求体中的数据
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            // 一定要使用 DataBufferUtils.release 释放掉, 否则, 会出现内存泄露
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });

        // 获取 request body
        return bodyRef.get();
    }
}

在conf文件夹下创建

/**
 * <h1>网关需要注入到容器中的 Bean</h1>
 * */
@Configuration
public class GatewayBeanConf {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

两种方式配置网关路由

在nacos界面的配置列表进行配置

在nacos界面的配置列表进行配置的添加:

 选择JSON格式,示例配置内容,这是nacos-client微服务部分的配置,以后的配置可以模仿这个。

[
    {
        "id": "e-commerce-nacos-client",
        "predicayes": [
            {
                "args": {
                    "pattern": "/shuai/ecommerce-nacos-client/**"
                },
                "name": "Path"
            }
        ],
        "uri": "lb://e-commerce-nacos-client",
        "filters": [
            {
                "name": "HeaderToken"
            },
            {
                "name": "StripPrefix",
                "args": {
                    "parts": "1"
                }
            }
        ]
    }
]

使用代码的方式配置

这种方法只适合比简单的配置,其实就是路由转发。

/**
 * <h1>配置登录请求转发规则</h1>
 * */
@Configuration
public class RouteLocatorConfig {

    /**
     * <h2>使用代码定义路由规则, 在网关层面拦截下登录和注册接口</h2>
     * */
    @Bean
    public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {

        // 手动定义 Gateway 路由规则需要指定 id、path 和 uri
        return builder.routes()
                .route(
                        "e_commerce_authority",
                        r -> r.path(
                                "/imooc/e-commerce/login",
                                "/imooc/e-commerce/register"
                        ).uri("http://localhost:9001/")
                ).build();
    }
}

将gateway,nacos,鉴权三个微服务启动,并使用http脚本进行验证

### 登录
POST http://localhost:9000/shuai/e-commerce/login
Content-Type: application/json

{
  "username": "shuai",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

### 注册
POST http://localhost:9000/shuai/e-commerce/register
Content-Type: application/json

{
  "username": "shuaizhale",
  "password": "25d55ad283aa400af464c76d713c07ad"
}
### 查询服务
GET http://localhost:9000/shuai/ecommerce-nacos-client/nacos-client/service-instance?serviceId=e-commerce-gateway
Accept: application/json
### 登录获得的JWT Token
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEyLFwidXNlcm5hbWVcIjpcInNodWFpemhhbGVcIn0iLCJqdGkiOiI3MTllOTE2Mi1hZGVhLTQ2MzItOWEzNS1hYzM4ZDE0NTcwNDIiLCJleHAiOjE2NDQ5NDA4MDB9.St8LrVs8Ym0etRzjgedCLfu-ZH_-u-fGi6-DQzYJu4yiEj0I5UaHAXE9tiXhSvM6Eu30Nlau7eg3qteLGhHKl8SzWaxhaZ0_Quo7gzU8_8zLZQCrkf_9lm_0mH5FHmTluJWRMxUsYTc0F6Yvf_G-jgY9A3wnM2Y8aFGciSfJZxFJ-DcWnrUrM3LCQhFxAunT3apCJ6Wa4wQKX6tNUsiTDvMJiVC3SIhqlUfB2N2d6cQvhlIUXLkMGU_SzyIzD1X6QZ4nu7LPwcNRRbcgtp_5SKYKvt_Pc7hXhg9-IyhVoGutNTpWVj9e5UbeuNze3iIRvyeclEJvjnEIMGqikJBzog
token: shuai
###

版权声明:本文为qq_41054749原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>