微服务(Spring Cloud)-- 02

一. 路由网关

1.1 认识网关

网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。

  • 网关可以做安全控制,也就是登录身份校验,校验通过才放行
  • 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去 Spring Cloud Gateway工作原理

客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则会将其发送到网关 Web 处理程序。 此处理程序通过特定于请求的筛选器链运行请求。 过滤器被虚线划分的原因是过滤器可以在发送代理请求之前和之后运行逻辑。 执行所有"预"筛选逻辑。然后发出代理请求。发出代理请求后,将运行"post"筛选逻辑。

1.2 路由网关基本使用

在pom中引入依赖

java 复制代码
<dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>

application.yaml文件配置路由

java 复制代码
server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848
    gateway:
      routes:
        - id: item-service
          uri: lb://item-service 
          predicates:
            - Path=/items/**,/search/**
        - id: cart-service
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**
        - id: trade-service
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: pay-service
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**
  • id:路由规则id,自定义,唯一
  • uri:路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
  • predicates:路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
  • Path这里是以请求路径作为判断规则

predicates路由断言其他的一些属性 官网:docs.spring.io/spring-clou...

1.3 路由过滤器

AddRequestHeader知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用,并在运行时展开。 以下示例配置了使用变量的 :AddRequestHeaderGatewayFilter

java 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        predicates:
          - Path=/red/{segment}
        filters:
          - AddRequestHeader=X-Request-Red, Blue-{segment}

AddResponseHeader知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用,并在运行时展开。 以下示例配置了使用变量的 :AddResponseHeaderGatewayFilter

java 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: add_response_header_route
        uri: https://example.org
        predicates:
          - Host: {segment}.myhost.org
        filters:
          - AddResponseHeader=foo, bar-{segment}

要添加过滤器并将其应用于所有路由,您可以使用默认过滤器spring.cloud.gateway.default-filters

java 复制代码
spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=X-Request-Red, Blue-{segment}
        - AddResponseHeader=X-Response-Default-Red, Default-Blue

还有很多很多种过滤器,见官网:docs.spring.io/spring-clou...

二. 网关登录校验

2.1 网关过滤器

  • 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  • WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  • Filter内部的逻辑分为pre和post两部分,分别会在请求路由到微服务之前和之后被执行。
  • 只有所有Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  • 微服务返回结果后,再倒序执行Filter的post逻辑。
  • 最终把响应结果返回。

2.2 自定义过滤器

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

1. 自定义GlobalFilter(最常用)

GlobalFilter:实现全局过滤器接口,使这个过滤器对所有路由生效 > Ordered:实现顺序接口,用于控制多个过滤器的执行顺序 ServerWebExchange:网关的"上下文对象",包含请求、响应、属性等全部信息 GatewayFilterChain:过滤器链,用于继续执行后续的过滤器(放行)

java 复制代码
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        System.out.println("headers = " + headers);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    	// 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}

2. 自定义GatewayFilter

java 复制代码
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("打印日志");
                return chain.filter(exchange);
            }
        }, 1);
    }
}

在yaml配置中使用:以自定义的GatewayFilterFactory类名称前缀类声明过滤器

java 复制代码
spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny

2.3 网关登录校验

解析jwt中的用户信息 保存用户到请求头

java 复制代码
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private final AuthProperties authProperties;
    private final JwtTool jwtTool;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取用户信息
        ServerHttpRequest request = exchange.getRequest();
        // 判断是否要做登录校验
        if (isExclude(request.getPath().toString())) {
            return chain.filter(exchange);
        }
        // 获取token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (headers != null && !headers.isEmpty()) {
            token = headers.get(0);
        }
        // 解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 拦截 回复401
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        // TODO 传递用户信息
        String userInfo = userId.toString();
        ServerWebExchange swe = exchange.mutate()
                .request(builder -> builder.header("user-info", userInfo))
                .build();
        // 放行
        return chain.filter(swe);
    }

    // 路径匹配 判断是否要做登录校验
    private boolean isExclude(String string) {
        for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, string)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

拦截器获取用户,拦截器放置在公共包hm-common下

java 复制代码
public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
            UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

hm-common模块下编写SpringMVC的配置类,配置登录拦截器:

java 复制代码
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MvcConfig

2.4 OpenFeign传递用户

我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。 但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:

Feign中提供的一个拦截器接口:feign.RequestInterceptor (和日志配置相同)参考 微服务(Spring Cloud) -- 01

java 复制代码
public class DefaultFeignConfig {
    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("userId", userId.toString());
                }
            }
        };
    }
}

调用的启动类配置:

java 复制代码
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)

三. 配置管理

目前的问题:

  • 网关路由在配置文件中写死了,如果变更必须重启微服务
  • 某些业务配置在配置文件中写死了,每次修改都要重启服务
  • 每个微服务都有很多重复的配置,维护成本高

3.1 配置共享

在nacos页面新增配置(配置管理 --> 配置列表 --> +)

拉取共享配置

SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

引入依赖:

java 复制代码
  <!--nacos配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>

在resources目录新建一个bootstrap.yaml文件:

java 复制代码
spring:
  application:
    name: cart-service
  profiles:
    active: local
  cloud:
    nacos:
      server-addr: 192.168.121.132:8848
      config:
        file-extension: yaml
        shared-configs:
          - dataId: shared-jdbc.yaml
          - dataId: shared-log.yaml
          - dataId: shared-swagger.yaml

修改application.yaml

java 复制代码
server:
  port: 8082
feign:
  okhttp:
    enabled: true
hm:
  db:
    database: hm-cart
  swagger:
    title: "黑马商城购物车接口文档"
    package: com.hmall.cart.controller

3.2 配置热更新

配置热更新是指在不重启应用的情况下,动态修改应用的配置并立即生效的技术。

在cart-service中新建一个属性读取类(CartProperties): 添加注解@ConfigurationProperties(prefix = "hm.cart")

java 复制代码
package com.hmall.cart.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
    private Integer maxAmount;
}

在nacos中添加一个配置文件 [服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 服务名:我们是购物车服务,所以是cart-service
  • spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
  • 后缀名:例如yaml

在业务中使用该属性加载类:

java 复制代码
private final CartProperties cartProperties;

3.3 配置网关动态路由

java 复制代码
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
        // 配置变更的通知处理
                System.out.println("recieve1:" + configInfo);
        }
        @Override
        public Executor getExecutor() {
                return null;
        }
});

示例: 监听nacos配置更新

网关gateway引入依赖:

java 复制代码
<!--统一配置管理-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

创建bootstrap.yaml文件

java 复制代码
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101
      config:
        file-extension: yaml
        shared-configs:
          - dataId: shared-log.yaml # 共享日志配置

在application.yml文件中,把之前的路由移除,用json格式添加到nacos中

java 复制代码
[
    {
        "id": "item",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
        }],
        "filters": [],
        "uri": "lb://item-service"
    },
    {
        "id": "cart",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/carts/**"}
        }],
        "filters": [],
        "uri": "lb://cart-service"
    },
    {
        "id": "user",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
        }],
        "filters": [],
        "uri": "lb://user-service"
    },
    {
        "id": "trade",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/orders/**"}
        }],
        "filters": [],
        "uri": "lb://trade-service"
    },
    {
        "id": "pay",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/pay-orders/**"}
        }],
        "filters": [],
        "uri": "lb://pay-service"
    }
]

这里核心的步骤有2步:

  • 创建ConfigService,目的是连接到Nacos
  • 添加配置监听器,编写配置变更的通知处理逻辑

第一步:由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配,因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了,因此,只要我们拿到NacosConfigManager就等于拿到了ConfigService。 第二步:编写监听器。虽然官方提供的SDK是ConfigService中的addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置。

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {
    private final NacosConfigManager nacosConfigManager;
    private final RouteDefinitionWriter routeDefinitionWriter;
    private final Set<String> routeIds = new HashSet<>();

    private final String dataId = "gateway-routes.json";
    private final String group = "DEFAULT_GROUP";
    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
        // 项目启动先拉取配置信息,并且添加配置监听器
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 5000, new Listener() {
                    // 线程池
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        // 配置变更,更新路由
                        updateConfigInfo(configInfo);
                    }
                });
        // 第一次读取到配置,也需要更新到路由
        updateConfigInfo(configInfo);
    }

    public void updateConfigInfo(String configInfo) {
        log.debug("更新路由表:{}", configInfo);
        // 解析配置信息,转为RouteDefinition
        List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
        // 删除旧的路由表
        for (String routeId : routeIds) {
            routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
        }
        // 清空旧的路由id
        routeIds.clear();
        // 更新路由表
        for (RouteDefinition routeDefinition : routeDefinitions) {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            // 记录路由id
            routeIds.add(routeDefinition.getId());
        }
    }
}
  • 总结: 创建和添加监听:NacosConfigManager接口 解析路由:RouteDefinition函数 更新路由:RouteDefinitionWriter接口
相关推荐
熙客4 小时前
Session与JWT安全对比
java·分布式·安全·web安全·spring cloud
程序员小潘6 小时前
Spring Gateway动态路由实现方案
后端·spring cloud
熙客7 小时前
分布式ID解决方案
java·分布式·spring cloud·微服务
whltaoin8 小时前
SpringCloud 项目阶段十:kafka实现双端信息同步以及ElasticSearch容器搭建示例
elasticsearch·spring cloud·kafka
龙茶清欢8 小时前
具有实际开发参考意义的 MyBatis-Plus BaseEntity 基类示例
java·spring boot·spring cloud·mybatis
齐 飞9 小时前
Spring Cloud Alibaba快速入门-Sentinel熔断规则
spring boot·spring cloud·sentinel
麦兜*10 小时前
Redis 7.0 新特性深度解读:迈向生产级的新纪元
java·数据库·spring boot·redis·spring·spring cloud·缓存
龙茶清欢10 小时前
最新版 springdoc-openapi-starter-webmvc-ui 常用注解详解 + 实战示例
java·spring boot·ui·spring cloud
whltaoin18 小时前
SpringCloud 项目阶段九:Kafka 接入实战指南 —— 从基础概念、安装配置到 Spring Boot 实战及高可用设计
spring boot·spring cloud·kafka