微服务网关与统一配置管理学习笔记

一、网关路由

1.1 认识网关

  • 定义:网关是网络的关口,负责数据在网络间传输时的路由、转发及安全校验。

  • 类比:类似园区传达室的大爷,控制外部访问,负责消息传递。

  • 微服务网关作用

    • 安全控制(登录身份校验)
    • 请求路由(根据请求判断访问哪个微服务并转发)
  • SpringCloud 网关实现方案

    两种方案

1.2 快速入门

通过创建网关微服务实现请求路由,步骤如下:

1.2.1 创建项目

在 hmall 下创建新 module,命名为 hm-gateway,作为网关微服务。

1.2.2 引入依赖

在 hm-gateway 模块的 pom.xml 文件中引入相关依赖,包括 common、网关、nacos discovery、负载均衡等依赖。

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
<?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>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>hm-gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<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>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

1.2.3 启动类

在 hm-gateway 模块的 com.hmall.gateway 包下新建启动类:

1
2
3
4
5
6
7
8
9
10
11
package com.hmall.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

1.2.4 配置路由

在 hm-gateway 模块的 resources 目录新建 application.yaml 文件,配置路由规则:

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
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/** # 以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

1.2.5 测试

启动 GatewayApplication,以 http://localhost:8080 拼接微服务接口路径测试,如 http://localhost:8080/items/page?pageNo=1&pageSize=1。启动相关微服务和前端页面,验证功能是否正常访问。

1.3 路由过滤

  • 路由规则定义语法
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
  • RouteDefinition 常见属性
    • id:路由的唯一标示
    • predicates:路由断言,匹配条件
    • filters:路由过滤条件
    • uri:路由目标地址,lb:// 代表负载均衡
  • SpringCloudGateway 支持的断言类型
    • After:某个时间点后的请求
    • Before:某个时间点之前的请求
    • Between:某两个时间点之间的请求
    • Cookie:请求必须包含某些 cookie
    • Header:请求必须包含某些 header
    • Host:请求必须是访问某个 host(域名)
    • Method:请求方式必须是指定方式
    • Path:请求路径必须符合指定规则
    • Query:请求参数必须包含指定参数
    • RemoteAddr:请求者的 ip 必须是指定范围
    • weight:权重处理

二、网关登录校验

2.1 鉴权思路分析

  • 问题:微服务拆分后,每个微服务独立部署,若每个都做登录校验,存在秘钥不安全和代码重复问题。
  • 解决方案:利用网关是所有微服务入口的特点,在网关进行登录校验。
  • 待解决问题
    • 如何在请求转发前做登录校验
    • 网关校验 JWT 后如何将用户信息传递给微服务
    • 微服务之间调用如何传递用户信息

2.2 网关过滤器

  • 网关请求处理流程
    1. 客户端请求进入网关后,HandlerMapping 判断请求,找到匹配的路由规则,交 WebHandler 处理。
    2. WebHandler 加载当前路由下的过滤器链,按顺序执行过滤器。
    3. 过滤器逻辑分 pre(请求路由到微服务前)和 post(微服务返回结果后)两部分。
    4. 所有 Filter 的 pre 逻辑执行通过后,请求路由到微服务。
    5. 微服务返回结果后,倒序执行 Filter 的 post 逻辑,最终返回响应结果。
  • 网关过滤器类型
    • GatewayFilter:路由过滤器,作用范围灵活,可指定路由。
    • GlobalFilter:全局过滤器,作用于所有路由,不可配置。
  • 过滤器方法签名
1
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
  • 内置 GatewayFilter 使用:无需编码,在 yaml 配置即可,可配置在指定 Route 或所有路由(default-filters)下。例如 AddRequestHeaderGatewayFilterFacotry:
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
- Path=/test/**
filters:
- AddRequestHeader=key, value
# 或作用于所有路由
# default-filters:
# - AddRequestHeader=key, value

2.3 自定义过滤器

2.3.1 自定义 GatewayFilter

实现 AbstractGatewayFilterFactory,类名以 GatewayFilterFactory 为后缀,在 yaml 配置使用。

  • 简单实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
System.out.println("过滤器执行了");
return chain.filter(exchange);
}
};
}
}
  • 支持动态配置参数实现:
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
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
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");
}

@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
  • 配置使用:
1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3
# 或
# - name: PrintAny
# args:
# a: 1
# b: 2
# c: 3

2.3.2 自定义 GlobalFilter

直接实现 GlobalFilter 和 Ordered 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("未登录,无法访问");
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

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

2.4 登录校验

利用自定义 GlobalFilter 完成登录校验。

2.4.1 JWT 工具

拷贝 hm-service 中的 JWT 相关工具,包括 AuthProperties、JwtProperties、SecurityConfig、JwtTool 及秘钥文件 hmall.jks,并在 application.yaml 配置相关属性:

1
2
3
4
5
6
7
8
9
10
11
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**

2.4.2 登录校验过滤器

定义登录校验的 GlobalFilter:

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
58
59
60
61
62
63
64
65
66
67
68
package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@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) {
ServerHttpRequest request = exchange.getRequest();
if(isExclude(request.getPath().toString())){
return chain.filter(exchange);
}
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
System.out.println("userId = " + userId);
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;
}
}

重启测试,验证无需登录的路径可访问,其他路径未登录时被拦截返回 401。

2.5 微服务获取用户

  • 流程:网关将用户信息以请求头方式传递给微服务,微服务通过拦截器从请求头获取并存入 ThreadLocal。

2.5.1 保存用户到请求头

改造网关过滤器,将用户信息保存到请求头。

2.5.2 拦截器获取用户

  • 在 hm-common 中定义拦截器,从请求头获取用户信息并保存到 ThreadLocal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userInfo = request.getHeader("user-info");
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}
  • 编写 SpringMVC 配置类配置拦截器,并在 resources/META-INF/spring.factories 中添加自动装配:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.hmall.common.config;

import com.hmall.common.interceptor.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig

2.5.3 恢复购物车代码

修改 cart-service 模块中 CartServiceImpl 的 queryMyCarts 方法,使用从 ThreadLocal 获取的用户信息。

2.6 OpenFeign 传递用户

微服务之间通过 OpenFeign 调用时,实现 feign.RequestInterceptor 接口传递用户信息。在 hm-api 模块的 DefaultFeignConfig 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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());
}
};
}