day04-微服务 02

YangeIT大约 36 分钟基础服务框架微服务

day04-微服务 02

课程内容

在昨天的作业中,我们将黑马商城拆分为 5 个微服务:

  • 用户服务
  • 商品服务
  • 购物车服务
  • 交易服务
  • 支付服务

由于每个微服务都有不同的地址或端口,入口不同,相信大家在与前端联调的时候发现了一些问题:

  • 请求不同数据时要访问不同的入口,需要维护多个入口地址,麻烦
  • 前端无法调用 nacos,无法实时更新服务列表

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,这就存在一些问题:

  • 每个微服务都需要编写登录校验、用户信息获取的功能吗?
  • 当微服务之间调用时,该如何传递用户信息?

不要着急,这些问题都可以在今天的学习中找到答案,我们会通过网关技术解决上述问题。

今天的内容会分为 3 章:

  • 第一章:网关路由,解决前端请求入口的问题。
  • 第二章:网关鉴权,解决统一登录校验和用户信息获取的问题。
  • 第三章:统一配置管理,解决微服务的配置文件重复和配置热更新问题。

通过今天的学习你将掌握下列能力:

  • 会利用微服务网关做请求路由
  • 会利用微服务网关做登录身份校验
  • 会利用 Nacos 实现统一配置管理
  • 会利用 Nacos 实现配置热更新

好了,接下来我们就一起进入今天的学习吧。

1.网关路由

1.1.认识网关

认识网关

网关:就是网络的关口,负责请求的路由、转发、身份校验。

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

更通俗的来讲,网关就像是以前园区传达室的大爷。

  • 外面的人要想进入园区,必须经过大爷的认可,如果你是不怀好意的人,肯定被直接拦截。
  • 外面的人要传话或送信,要找大爷。大爷帮你带给目标人。 image

现在,微服务网关就起到同样的作用。前端请求不能直接访问微服务,而是要请求网关:

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

在 SpringCloud 当中,提供了两种网关实现方案:

  • Netflix Zuul [zuːl]:早期实现,目前已经淘汰
  • SpringCloudGateway:基于 Spring 的 WebFlux 技术,完全支持响应式编程,吞吐能力更强

课堂中以 SpringCloudGateway 为例来讲解,官方网站:

https://spring.io/projects/spring-cloud-gateway/open in new window

image
image

总结

课堂作业

  1. 什么是网关?有何作用?🎤
  2. SpringCloud提供了哪些网关解决方案?哪种方案更受欢迎?🎤

1.2.SpringCloudGateway快速入门

SpringCloudGateway快速入门

接下来,先看下如何利用网关实现请求路由。由于网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下:

  • 创建网关微服务
  • 引入 SpringCloudGateway、NacosDiscovery 依赖
  • 编写启动类
  • 配置网关路由
image
image

代码操作

1.2.1.创建项目

首先,我们要在 hmall 下创建一个新的 module,命名为 hm-gateway,作为网关微服务:

image
image

总结

课堂作业

  1. SpringCloudGateway是什么?什么作用?🎤
  2. 参考上述步骤,插入网关模块

1.3.路由过滤

路由过滤

路由规则的定义语法如下:

spring:
  cloud:
    gateway:
      routes:
        - id: item # 路由规则id,自定义,唯一
          uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
          predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则

GatewayProperties.java其中 routes 对应的类型如下:

imageimage

是一个集合,也就是说可以定义很多路由规则。

集合中的 RouteDefinition 就是具体的路由规则定义,其中常见的属性如下:

四个属性含义如下:

  • id:路由的唯一标示
  • predicates:路由断言,其实就是匹配条件
  • filters:路由过滤条件,后面讲
  • uri:路由目标地址,lb:// 代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

这里我们重点关注 predicates,也就是路由断言。

总结

课堂作业

  1. 路由规则中的uri属性是什么意思? lb://有何作用 🎤
  2. 路由规则id有什么特性? 🎤
  3. 路由断言的有作用是什么? 🎤
  4. 参考上述步骤,体验一下路由断言的作用 ✏️

2.网关登录校验

2.1 鉴权

鉴权

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。

微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

鉴权思路分析

我们的登录是基于 JWT 来实现的,校验 JWT 的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题

  • 每个微服务都需要知道 JWT 的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

此时,登录校验的流程如图:

image
image

不过,这里存在几个问题:

  • 网关路由是配置的,请求转发是 Gateway 内部代码,我们如何在转发之前做登录校验?
  • 网关校验 JWT 之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

这些问题将在接下来几节一一解决。

总结

课堂作业

  1. 微服务拆分后,登录校验存在哪些问题?🎤

2.2.网关过滤器

本节目标:了解网关工作原理和熟悉自定义网关过滤器的方式 🎯

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。

网关的请求转发是 Gateway 内部代码实现的,要想在请求转发之前做登录校验,就必须了解 Gateway 内部工作的基本原理。

image
image

如图所示:

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

如图中所示,最终请求转发是有一个名为 NettyRoutingFilter 的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了!

GatewayFilter实操需求

需求:给所有请求的请求头,添加: teacher:yangeit

  1. 在Gateway微服务中,添加请求头
spring: # 顶格写
  application:
    name: gateway
    gateway:
      routes:
        - id: cart
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
            - After=2023-12-06T15:14:47.433+08:00[Asia/Shanghai]
          filters:
            - AddRequestHeader=teacher, yangeit











 
  1. 在cart购物车微服务中CartController类中,接收请求头信息并打印
   @ApiOperation("查询购物车列表")
    @GetMapping
    public List<CartVO> queryMyCarts(HttpServletRequest request){
        String teacher = request.getHeader("teacher");
        log.info("teacher:{}",teacher);

        return cartService.queryMyCarts();
    }


 
 
 



运行截图: image

GlobalFilter实操需求

需求:拦截所有请求,直接返回401错误

自定义GlobalFilter

package com.hmall.gateway.filter;
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 编写过滤器逻辑
        System.out.println("未登录,无法访问");
        // 放行
        // return chain.filter(exchange);

        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.setRawStatusCode(401);
        return response.setComplete();
    }

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

点击查看运行截图open in new window 👈

点击查看前端运行效果图open in new window👈

总结

课堂作业

  1. GatewayFilter路由过滤器中的AddRequestHeader过滤器有什么作用?🎤
  2. 参考上述操作,完成内置的路由过滤器和自定义GlobalFilter全局过滤器的体验 ✏️

2.4.登录校验

登录校验

接下来,我们就利用自定义 GlobalFilter 来完成登录校验。

JWT 工具

登录校验需要用到 JWT,而且 JWT 的加密需要秘钥和加密工具。这些在 hm-service 中已经有了,我们直接拷贝过来:

具体作用如下:

  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
  • JwtProperties:定义与 JWT 工具有关的属性,比如秘钥文件位置
  • SecurityConfig:工具的自动装配
  • JwtTool:JWT 工具,其中包含了校验和解析 token 的功能
  • hmall.jks:秘钥文件

其中 AuthPropertiesJwtProperties 所需的属性要在 application.yaml 中配置:

hm:
  jwt:
    location: classpath:hmall.jks # 秘钥地址
    alias: hmall # 秘钥别名
    password: hmall123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**

总结

课堂作业

  1. 说一下登录验证过滤器的工作流程? 有什么用?🎤
  2. 看图回答问题?open in new window 👈

2.5.微服务获取用户

本节目标:网关将请求转发到微服务时,微服务获取网关中设置的用户身份 🎯

微服务获取用户

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢? 🎯

由于网关发送请求到微服务依然采用的是 Http 请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用 SpringMVC 的拦截器来实现登录用户信息获取,并存入 ThreadLocal,方便后续使用。

流程图如下:

因此,接下来我们要做的事情有:

  1. 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
  2. 编写微服务拦截器,拦截请求获取用户信息,保存到 ThreadLocal 后放行

代码操作

2.5.1.保存用户到请求头

首先,我们修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:

image
image

总结

课堂作业

  1. 网关通过什么将用户id传给了微服务?微服务怎么保障userid在程序内部传递?🎤

2.6.OpenFeign 传递用户

本节目标:实现微服务(非网关)与微服务之间的数据传递 🎯

OpenFeign传递用户

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,

流程如下:

下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!

由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头

总结

课堂作业

  1. 服务之间怎么传递用户信息?实现思路是怎样的?🎤

3.配置管理

3.1.配置共享

配置管理-配置共享

到目前为止我们已经解决了微服务相关的几个问题:

  • 微服务远程调用 🆗
  • 微服务注册、发现 🆗
  • 微服务请求路由、负载均衡 🆗
  • 微服务登录用户信息传递 🆗

不过,现在依然还有几个问题需要解决:

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

这些问题都可以通过统一的配置管理器服务解决 🎯。而 Nacos 不仅仅具备注册中心功能,也具备配置管理的功能:

点击查看集群图解open in new window 👈

微服务共享的配置可以统一交给 Nacos 保存和管理,在 Nacos 控制台修改配置后,Nacos 会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新

注:网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。

案例代码操作

需求

我们可以把微服务共享的配置抽取到 Nacos 中统一管理,这样就不需要每个微服务都重复配置了。分为两大步:

  • 在 Nacos 中添加共享配置
    • 创建shared-jdbc.yaml配置文件存放数据库相关的配置
    • 创建shared-log.yaml配置文件存放log日志相关的配置
    • 创建shared-swagger.yaml配置文件存放接口文档相关的配置
  • 微服务拉取配置
    • 引入依赖alibaba-nacos-configstarter-bootstrap依赖
    • 新建 bootstrap.yaml并配置nacos地址和配置信息
    • 删减 application.yaml文件信息

1.添加共享配置

cart-service 为例,我们看看有哪些配置是重复的,可以抽取的:

首先是 jdbc 相关配置:

然后是日志配置:

然后是 swagger 以及 OpenFeign 的配置:

image
image

我们在 nacos 控制台分别添加这些配置。

1️⃣ 抽取数据库公共配置

首先是 jdbc 相关配置shared-jdbc.yaml,在 配置管理->配置列表 中点击 + 新建一个配置:

在弹出的表单中填写信息:

image
image

其中shared-jdbc.yaml详细的配置如下:

spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.138.135}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${hm.db.un:root}
    password: ${hm.db.pw:1234}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto

注意这里的 jdbc 的相关参数并没有写死,例如:

  • 数据库ip:通过 ${hm.db.host:192.168.138.135} 配置了默认值为 192.168.138.135,同时允许通过 ${hm.db.host} 来覆盖默认值
  • 数据库端口:通过 ${hm.db.port:3306} 配置了默认值为 3306,同时允许通过 ${hm.db.port} 来覆盖默认值
  • 数据库database:可以通过 ${hm.db.database} 来设定,无默认值
2️⃣ 抽取log日志相关的配置

然后是统一的日志配置,命名为 shared-log.yaml,配置内容如下:

logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"
image
image
3️⃣ 抽取接口文档相关的配置

然后是统一的 swagger 配置,命名为 shared-swagger.yaml,配置内容如下:

knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:黑马商城接口文档}
    description: ${hm.swagger.description:黑马商城接口文档}
    email: ${hm.swagger.email:huyan@itcast.cn}
    concat: ${hm.swagger.concat:yangeit}
    url: http://www.yangeit.cn:21010/
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${hm.swagger.package}
image
image

注意,这里的 swagger 相关配置我们没有写死,例如:

  • title:接口文档标题,我们用了 ${hm.swagger.title} 来代替,将来可以有用户手动指定
  • email:联系人邮箱,我们用了 ${hm.swagger.email:``huyan@itcast.cn``},默认值是 huyan@itcast.cn,同时允许用户利用 ${hm.swagger.email} 来覆盖。

完成截图:👇 image

总结

课堂作业

  1. bootstrap.yaml文件和application.yaml文件的优先级是怎样的?🎤
  2. 配置中心能解决什么问题?🎤
  3. 参考上述的步骤,完成配置共享操作✏️

3.2.配置热更新

配置热更新

有很多的业务相关参数,将来可能会根据实际情况临时调整。

例如购物车业务,购物车数量有一个上限,默认是 10,对应代码如下:

image
image

现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。

但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?

这就要用到 Nacos 的配置热更新能力了,分为两步:

  • 在 Nacos 中添加配置
  • 在微服务读取配置

代码操作

1.添加配置到 Nacos

首先,我们在 nacos 中添加一个配置文件cart-service.yaml,将购物车的上限数量添加到配置中:

注意文件的 dataId 格式:

[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

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

这里我们直接使用 cart-service.yaml 这个名称通用性强则不管是 dev 还是 local 环境都可以共享该配置

配置内容如下:

hm:
  cart:
    maxAmount: 2 # 购物车商品数量上限
image
image

提交配置,在控制台能看到新添加的配置:

image
image

总结

课堂作业

  1. 配置热跟新有什么作用?或者说有哪些应用场景?🎤

3.3.动态路由

前言

网关的路由配置全部是在项目启动时由 org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator 在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个 Map),不会改变。也不会监听路由变更,所以,我们无法利用上节课学习的配置热更新来实现路由更新。

因此,我们必须监听 Nacos 的配置变更,然后手动把最新的路由更新到路由表中。

image
image

这里有两个难点:

  • 如何监听 Nacos 配置变更?
  • 如何把路由信息更新到路由表?

代码操作

1.监听 Nacos 配置变更

在 Nacos 官网中给出了手动监听 Nacos 配置变更的 SDK:

官网链接:https://nacos.io/zh-cn/docs/sdk.htmlopen in new window

image
image

阅读官网,了解一下有这么回事儿,并且了解以下核心步骤 🎯

核心的步骤: 👇

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

由于我们采用了 spring-cloud-starter-alibaba-nacos-config 自动装配,因此 ConfigService 已经 com.alibaba.cloud.nacos.NacosConfigAutoConfiguration 中自动创建好了:

image
image

因此,只要我们拿到 NacosConfigManager 就等于拿到了 ConfigService,第一步就实现了。

2️⃣ 添加配置监听器,编写配置变更的通知处理逻辑

虽然官方提供的 SDK 是 ConfigService 中的 addListener,项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的 API 是这个:getConfigAndSignListener,既可以配置监听器,并且会根据 dataId 和 group 读取配置并返回。

我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。

官网链接:https://nacos.io/zh-cn/docs/sdk.htmlopen in new window

2.1 参考官方代码,在gateway微服务中创建动态路由加载器:DynamicRouteLoader


/**
 * @author yangeit
 * @date 2023年12月10日
 * @Description
 */
@Slf4j
@Component
public class DynamicRouteLoader {

    @Autowired
    private  NacosConfigManager nacosConfigManager;

    // 路由配置文件的id和分组
    private final String dataId = "xxxx";
    private final String group = "xxxx";


    @PostConstruct //实体类初始方法,在狗造之后执行
    public void initRouteConfigListener() throws NacosException {
        // 1.注册监听器并首次拉取配置
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 5000, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        log.info("网关接收配置信息:{}",configInfo);
                    }
                });
        // 2.首次启动时,更新一次配置
        log.info("网关首次启动时,更新一次配置");
    }
}

接下来思考,如何将远程的配置跟新到本地! 🎯

课后作业

🚩 1. 重点完成上述的课堂作业

  1. 晚自习第一节课的前30分钟,总结完毕之后,每个同学先必须梳理今日知识点 (记得写不知道的,以及感恩三件事);整理好的笔记可以发给组长,组长交给班长,意在培养大家总结的能力)

  2. 晚自习第一节课的后30分钟开始练习(记住:程序员是代码堆起来的):

    • 先要把今天的所有案例或者课堂练习,如果没练完的,练完他
    • 拆分微服务 👈
  3. 剩余的时间:预习第二天的知识,预习的时候一定要注意:

  • 预习不是学习,不要死看第二天的视频(很容易出现看了白看,为了看视频而看视频)
  • 预习看第二天的笔记,把笔记中标注重要的知识,可以找到预习视频,先看一遍,如果不懂的 ,记住做好标注。

4.作业:拆分微服务

作业

4.1.拆分微服务

将项目一拆分为一个微服务项目,并完成下列需求:

  • 基于 OpenFeign 实现服务间远程调用
  • 定义网关,实现对微服务的请求路由
  • 基于网关实现登录用户校验和用户信息传递

以苍穹外卖为例,项目可以拆分为:

  • 业务服务:

    • 用户服务:用户、地址、登录等相关业务
    • 产品服务:店铺、分类、菜品、套餐等业务
    • 交易服务:订单、购物车业务
    • 数据服务:工作台、报表统计等业务
  • 基础服务:

    • 支付服务:支付相关业务
    • 文件服务:文件上传功能