微服务网关与统一配置管理学习笔记
一、网关路由
1.1 认识网关

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> <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> <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 uri: lb://item-service 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 网关过滤器
- 网关请求处理流程:
- 客户端请求进入网关后,HandlerMapping 判断请求,找到匹配的路由规则,交 WebHandler 处理。
- WebHandler 加载当前路由下的过滤器链,按顺序执行过滤器。
- 过滤器逻辑分 pre(请求路由到微服务前)和 post(微服务返回结果后)两部分。
- 所有 Filter 的 pre 逻辑执行通过后,请求路由到微服务。
- 微服务返回结果后,倒序执行 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
|
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
|
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()); } }; }
|