网关

一、网关快速入门

1.1 认识网关

  当项目采用微服务架构时,由于每个服务有自己服务器端口,这样产生了以下两个问题:

  • 后端服务器接口过多,而且后期可能会进行更改或者扩展,因此前端对后端不同服务的请求会很复杂,难以管理;
  • 微服务的多个服务可能会有登录请求校验,获得用户信息等功能,如果在多个模块中添加相应的功能代码,会导致程序冗余。

  为解决以上问题,就需要一个同一管理后端微服务接口的中间件,这就是路由网关。网关对外暴露一个端口,随后根据请求的URL将请求路由转发给提供相应服务的后端微服务,同时可以在网关设置登录校验等统一功能。

  在SpringCloud有两种网管实现方案,现在一般采用SpringCloudGateway(https://spring.io/projects/spring-cloud-gateway#learn)的解决方案。

1.2 使用步骤

  1. 步骤一:创建新模块
  2. 步骤二:引入依赖:SpringCloudGateway、nacos发现以及负载均衡依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!--网关-->
    <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>
  3. 步骤三:新建启动类
  4. 步骤四:配置路由:路由信息就是根据接收到前端请求的url来路由转发到已在nacos注册服务的后端微服务地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    server:
    port: 8080
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: 192.168.150.101:8848
    gateway:
    routes:
    - id: item # 路由规则id,自定义,唯一
    uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
    predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
    - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
    default-filters:

1.3 路由过滤

  在网关的配置文件中,需要填写后端微服务的路由信息,其中包括四个信息

  • id:唯一标识一个路由的id,可自定义;
  • uri:表示微服务的地址,格式:lb://item-service
  • predicates:路由匹配的条件,时key-value结构,可支持的类型有很多
  • filter:SpringCloudGateway提供了33种过滤器,其中每种过滤器都有独特的功能。若配置在routers下,则只对当前路由生效;如果想对所有路由生效,那么需要一个default-filters属性

1.4 自定义过滤器

自定义过滤器分为两种:
GatewayFilter:作用于指定的路由请求,默认不生效,要配置到路由后才生效
GlobalFilter:作用范围是所有路由,声明后自动生效

Global过滤器
  实现GlobalFilter接口的filter方法,用于编写过滤逻辑;实现Ordered接口的getOrder方法,用于表示过滤器的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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;
}
}

GatewayFilter
  实现AbstractGatewayFilterFactory接口的apply方法,这个实现支持动态配置参数且需要配置后才能生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}

// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}

}

  随后在网关配置文件中配置该过滤器

1
2
3
4
5
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

二、网关登录校验

2.1 网关请求处理流程

  • 前端发送的请求首先会到达网关的路由映射器,这一步会找到与请求匹配的路由
  • 接下来会到达请求处理器,它将网关中的所有的过滤器根据优先级形成一条过滤器链
  • 随后依次到达过滤器的pre部分,这是到达微服务前的处理逻辑,微服务给出结果后,还会经过过滤器的post部分。

在这过程中,有如下几个问题待处理

  • 如何在网关将请求转发到微服务前做登陆验证
  • 网关如何把请求转发给微服务
  • 微服务间如何传递用户消息

2.2 登录校验过滤器

  在网关的filter文件夹下新建一个Global校验过滤器,这样解决了在网关中做登录校验的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final JwtTool jwtTool;

private final AuthProperties authProperties;

private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// TODO 5.如果有效,传递用户信息
System.out.println("userId = " + userId);
// 6.放行
return chain.filter(exchange);
}

private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}

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

在登陆校验中用到了token,用到了ConfigurationProperties(prefix = “hm.jwt”)注解,该注解可以获取到配置文件的数据,只需要制定好配置文件中的前缀即可

2.3 网关传送用户消息给微服务

  在上一小节完成的登录校验过滤器中,对请求头中的token进行了检验,对检验成功的token提取出了UserId,但是并没有将UserId传送给下游微服务。
  在filter方法中的exchange参数包含了http请求的所有内容,因此可以将UserId通过exchange保存到请求头中,再传递给下游服务,于是需要将上一小节第5步修改如下:

1
2
3
4
5
6
7
// 5. 传递用户信息到微服务中
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 6. 返回
return chain.filter(swe);

  刚刚只是在网关部分将用户信息保存到请求头中并发送给了下游微服务,因此在微服务中需要定义一个过滤器读取请求头中的用户信息保存到ThreadLocal中。
  而所有微服务都需要或许用户信息,因此我们在common模块中定义一个过滤器,这样所有的微服务都可以通过该过滤器获得用户信息。

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}

  由于过滤器实现比较简单,因此这里没有给出具体代码。而在过滤器注册代码中,需要注意一点,由于gateway也用到了common模块的部分功能,但是gateway模块并没有引入springMVC的依赖,因此需要加上@ConditionalOnClass(DispatcherServlet.class)注解,代表这个过滤器只在含有SpringMVC的模块中生效。

2.4 微服务之间传递用户信息

  经过以上步骤,前端对某一个微服务发起的请求,已经可以通过网关进行身份校验,路由转发,下游微服务可以也可以正确接收到网管转发的包含用户信息的请求。
  但是当微服务之间有调用关系时,此时可能需要传递用户信息,这个步骤是通过feign发送http请求完成的,但是在发送该请求时并没有添加用户信息。为解决这个问题,只需要实现feign.RequestInterceptor接口的apply方法即可,可以通过该方法中template参数提供的方法来对请求头做各种各样的修改。
  这里为了方便,在统一接口hm-api中使用匿名内部类的方法实现。每一次通过feign发起http微服务内部请求时都会自动调用该接口中apply方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}

三、配置管理

3.1 spring-cloud配置文件管理

  在SpringCloud项目中,首先会尝试加载bootstrap.yml文件,随后尝试拉取nacos中的配置,随后加载application.yml文件,最后合并两个配置文件。

3.2 共享配置

  在所有微服务的配置文件中,有很多共享的配置信息,例如数据库mysql、mybatis-plus以及日志文件的配置,我们可以通过nacos提供的共享配置功能来简化配置文件

  1. 步骤一:首先在提取出微服务中的共享配置,并将配置添加到微服务的配置管理列表中,相当于完成配置的注册

  2. 步骤二:从nacos中拉取共享配置信息

    1. 引入nacos配置管理依赖以及读取bootstarp文件的依赖
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!--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>
    2. bootstrap.yaml文件中配置如下信息,包括服务名称、nacos地址以及共享配置ID
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      spring:
      application:
      name: cart-service # 服务名称
      profiles:
      active: dev
      cloud:
      nacos:
      server-addr: 192.168.223.101 # nacos地址
      config:
      file-extension: yaml # 文件后缀名
      shared-configs: # 共享配置
      - dataId: shared-jdbc.yaml # 共享mybatis配置
      - dataId: shared-log.yaml # 共享日志配置
      - dataId: shared-swagger.yaml # 共享日志配置
    3. 删除application.yaml文件中荣誉的配置

3.3 配置热更新

  在3.1小节中,提到了会拉取nacos配置,在这一步中除了会拉取在bootstrap.yaml文件中配置的共享文件,还会尝试拉取以下两个文件,分别是:

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

步骤

  1. 步骤一:在nacos配置管理中添加一个ID符合上述名称的配置文件

  2. 步骤二:在cart-service的config包下读取配置信息

    1
    2
    3
    4
    5
    6
    @Data
    @Component
    @ConfigurationProperties(prefix = "hm.cart")
    public class CartProperties {
    private Integer maxAmount;
    }